Merge branch 'develop' into data_handler
This commit is contained in:
commit
fc2970f41b
@ -2,6 +2,7 @@
|
|||||||
"max_open_trades": 3,
|
"max_open_trades": 3,
|
||||||
"stake_currency": "BTC",
|
"stake_currency": "BTC",
|
||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
|
"tradable_balance_ratio": 0.99,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"ticker_interval" : "5m",
|
"ticker_interval" : "5m",
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
@ -59,7 +60,6 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"process_throttle_secs": 3600,
|
"process_throttle_secs": 3600,
|
||||||
"calculate_since_number_of_days": 7,
|
"calculate_since_number_of_days": 7,
|
||||||
"capital_available_percentage": 0.5,
|
|
||||||
"allowed_risk": 0.01,
|
"allowed_risk": 0.01,
|
||||||
"stoploss_range_min": -0.01,
|
"stoploss_range_min": -0.01,
|
||||||
"stoploss_range_max": -0.1,
|
"stoploss_range_max": -0.1,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"max_open_trades": 3,
|
"max_open_trades": 3,
|
||||||
"stake_currency": "BTC",
|
"stake_currency": "BTC",
|
||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
|
"tradable_balance_ratio": 0.99,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"ticker_interval" : "5m",
|
"ticker_interval" : "5m",
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
@ -64,7 +65,6 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"process_throttle_secs": 3600,
|
"process_throttle_secs": 3600,
|
||||||
"calculate_since_number_of_days": 7,
|
"calculate_since_number_of_days": 7,
|
||||||
"capital_available_percentage": 0.5,
|
|
||||||
"allowed_risk": 0.01,
|
"allowed_risk": 0.01,
|
||||||
"stoploss_range_min": -0.01,
|
"stoploss_range_min": -0.01,
|
||||||
"stoploss_range_max": -0.1,
|
"stoploss_range_max": -0.1,
|
||||||
|
@ -2,8 +2,11 @@
|
|||||||
"max_open_trades": 3,
|
"max_open_trades": 3,
|
||||||
"stake_currency": "BTC",
|
"stake_currency": "BTC",
|
||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
|
"tradable_balance_ratio": 0.99,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"amount_reserve_percent" : 0.05,
|
"amount_reserve_percent" : 0.05,
|
||||||
|
"amend_last_stake_amount": false,
|
||||||
|
"last_stake_amount_min_ratio": 0.5,
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"ticker_interval": "5m",
|
"ticker_interval": "5m",
|
||||||
"trailing_stop": false,
|
"trailing_stop": false,
|
||||||
@ -96,7 +99,6 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"process_throttle_secs": 3600,
|
"process_throttle_secs": 3600,
|
||||||
"calculate_since_number_of_days": 7,
|
"calculate_since_number_of_days": 7,
|
||||||
"capital_available_percentage": 0.5,
|
|
||||||
"allowed_risk": 0.01,
|
"allowed_risk": 0.01,
|
||||||
"stoploss_range_min": -0.01,
|
"stoploss_range_min": -0.01,
|
||||||
"stoploss_range_max": -0.1,
|
"stoploss_range_max": -0.1,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"max_open_trades": 5,
|
"max_open_trades": 5,
|
||||||
"stake_currency": "EUR",
|
"stake_currency": "EUR",
|
||||||
"stake_amount": 10,
|
"stake_amount": 10,
|
||||||
|
"tradable_balance_ratio": 0.99,
|
||||||
"fiat_display_currency": "EUR",
|
"fiat_display_currency": "EUR",
|
||||||
"ticker_interval" : "5m",
|
"ticker_interval" : "5m",
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
@ -70,7 +71,6 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"process_throttle_secs": 3600,
|
"process_throttle_secs": 3600,
|
||||||
"calculate_since_number_of_days": 7,
|
"calculate_since_number_of_days": 7,
|
||||||
"capital_available_percentage": 0.5,
|
|
||||||
"allowed_risk": 0.01,
|
"allowed_risk": 0.01,
|
||||||
"stoploss_range_min": -0.01,
|
"stoploss_range_min": -0.01,
|
||||||
"stoploss_range_max": -0.1,
|
"stoploss_range_max": -0.1,
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 211 KiB |
BIN
docs/assets/plot-dataframe2.png
Normal file
BIN
docs/assets/plot-dataframe2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 190 KiB |
@ -78,9 +78,11 @@ Please also read about the [strategy startup period](strategy-customization.md#s
|
|||||||
#### Supplying custom fee value
|
#### Supplying custom fee value
|
||||||
|
|
||||||
Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt.
|
Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt.
|
||||||
To account for this in backtesting, you can use `--fee 0.001` to supply this value to backtesting.
|
To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting.
|
||||||
This fee must be a ratio, and will be applied twice (once for trade entry, and once for trade exit).
|
This fee must be a ratio, and will be applied twice (once for trade entry, and once for trade exit).
|
||||||
|
|
||||||
|
For example, if the buying and selling commission fee is 0.1% (i.e., 0.001 written as ratio), then you would run backtesting as the following:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --fee 0.001
|
freqtrade backtesting --fee 0.001
|
||||||
```
|
```
|
||||||
|
@ -40,9 +40,12 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades).<br> ***Datatype:*** *Positive integer or -1.*
|
| `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades). [More information below](#configuring-amount-per-trade).<br> ***Datatype:*** *Positive integer or -1.*
|
||||||
| `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *String*
|
| `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *String*
|
||||||
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Positive float or `"unlimited"`.*
|
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Positive float or `"unlimited"`.*
|
||||||
|
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> ***Datatype:*** *Positive float between `0.1` and `1.0`.*
|
||||||
|
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean*
|
||||||
|
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> ***Datatype:*** *Float (as ratio)*
|
||||||
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> ***Datatype:*** *Positive Float as ratio.*
|
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> ***Datatype:*** *Positive Float as ratio.*
|
||||||
| `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *String*
|
| `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *String*
|
||||||
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> ***Datatype:*** *String*
|
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> ***Datatype:*** *String*
|
||||||
@ -131,20 +134,58 @@ Values set in the configuration file always overwrite values set in the strategy
|
|||||||
* `sell_profit_only` (ask_strategy)
|
* `sell_profit_only` (ask_strategy)
|
||||||
* `ignore_roi_if_buy_signal` (ask_strategy)
|
* `ignore_roi_if_buy_signal` (ask_strategy)
|
||||||
|
|
||||||
### Understand stake_amount
|
### Configuring amount per trade
|
||||||
|
|
||||||
The `stake_amount` configuration parameter is an amount of crypto-currency your bot will use for each trade.
|
There are several methods to configure how much of the stake currency the bot will use to enter a trade. All methods respect the [available balance configuration](#available-balance) as explained below.
|
||||||
|
|
||||||
The minimal configuration value is 0.0001. Please check your exchange's trading minimums to avoid problems.
|
#### Available balance
|
||||||
|
|
||||||
|
By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade.
|
||||||
|
Freqtrade will reserve 1% for eventual fees when entering a trade and will therefore not touch that by default.
|
||||||
|
|
||||||
|
You can configure the "untouched" amount by using the `tradable_balance_ratio` setting.
|
||||||
|
|
||||||
|
For example, if you have 10 ETH available in your wallet on the exchange and `tradable_balance_ratio=0.5` (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers this as available balance. The rest of the wallet is untouched by the trades.
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
The `tradable_balance_ratio` setting applies to the current balance (free balance + tied up in trades). Therefore, assuming the starting balance of 1000, a configuration with `tradable_balance_ratio=0.99` will not guarantee that 10 currency units will always remain available on the exchange. For example, the free amount may reduce to 5 units if the total balance is reduced to 500 (either by a losing streak, or by withdrawing balance).
|
||||||
|
|
||||||
|
#### Amend last stake amount
|
||||||
|
|
||||||
|
Assuming we have the tradable balance of 1000 USDT, `stake_amount=400`, and `max_open_trades=3`.
|
||||||
|
The bot would open 2 trades, and will be unable to fill the last trading slot, since the requested 400 USDT are no longer available, since 800 USDT are already tied in other trades.
|
||||||
|
|
||||||
|
To overcome this, the option `amend_last_stake_amount` can be set to `True`, which will enable the bot to reduce stake_amount to the available balance in order to fill the last trade slot.
|
||||||
|
|
||||||
|
In the example above this would mean:
|
||||||
|
|
||||||
|
- Trade1: 400 USDT
|
||||||
|
- Trade2: 400 USDT
|
||||||
|
- Trade3: 200 USDT
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
This option only applies with [Static stake amount](#static-stake-amount) - since [Dynamic stake amount](#dynamic-stake-amount) divides the balances evenly.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
The minimum last stake amount can be configured using `amend_last_stake_amount` - which defaults to 0.5 (50%). This means that the minimum stake amount that's ever used is `stake_amount * 0.5`. This avoids very low stake amounts, that are close to the minimum tradable amount for the pair and can be refused by the exchange.
|
||||||
|
|
||||||
|
#### Static stake amount
|
||||||
|
|
||||||
|
The `stake_amount` configuration statically configures the amount of stake-currency your bot will use for each trade.
|
||||||
|
|
||||||
|
The minimal configuration value is 0.0001, however, please check your exchange's trading minimums for the stake currency you're using to avoid problems.
|
||||||
|
|
||||||
This setting works in combination with `max_open_trades`. The maximum capital engaged in trades is `stake_amount * max_open_trades`.
|
This setting works in combination with `max_open_trades`. The maximum capital engaged in trades is `stake_amount * max_open_trades`.
|
||||||
For example, the bot will at most use (0.05 BTC x 3) = 0.15 BTC, assuming a configuration of `max_open_trades=3` and `stake_amount=0.05`.
|
For example, the bot will at most use (0.05 BTC x 3) = 0.15 BTC, assuming a configuration of `max_open_trades=3` and `stake_amount=0.05`.
|
||||||
|
|
||||||
To allow the bot to trade all the available `stake_currency` in your account set
|
!!! Note
|
||||||
|
This setting respects the [available balance configuration](#available-balance).
|
||||||
|
|
||||||
```json
|
#### Dynamic stake amount
|
||||||
"stake_amount" : "unlimited",
|
|
||||||
```
|
Alternatively, you can use a dynamic stake amount, which will use the available balance on the exchange, and divide that equally by the amount of allowed trades (`max_open_trades`).
|
||||||
|
|
||||||
|
To configure this, set `stake_amount="unlimited"`. We also recommend to set `tradable_balance_ratio=0.99` (99%) - to keep a minimum balance for eventual fees.
|
||||||
|
|
||||||
In this case a trade amount is calculated as:
|
In this case a trade amount is calculated as:
|
||||||
|
|
||||||
@ -152,6 +193,16 @@ In this case a trade amount is calculated as:
|
|||||||
currency_balance / (max_open_trades - current_open_trades)
|
currency_balance / (max_open_trades - current_open_trades)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To allow the bot to trade all the available `stake_currency` in your account (minus `tradable_balance_ratio`) set
|
||||||
|
|
||||||
|
```json
|
||||||
|
"stake_amount" : "unlimited",
|
||||||
|
"tradable_balance_ratio": 0.99,
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available).
|
||||||
|
|
||||||
!!! Note "When using Dry-Run Mode"
|
!!! Note "When using Dry-Run Mode"
|
||||||
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
|
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ Edge module has following configuration options:
|
|||||||
| `enabled` | If true, then Edge will run periodically. <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean*
|
| `enabled` | If true, then Edge will run periodically. <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean*
|
||||||
| `process_throttle_secs` | How often should Edge run in seconds. <br>*Defaults to `3600` (once per hour).* <br> ***Datatype:*** *Integer*
|
| `process_throttle_secs` | How often should Edge run in seconds. <br>*Defaults to `3600` (once per hour).* <br> ***Datatype:*** *Integer*
|
||||||
| `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy. <br> **Note** that it downloads historical data so increasing this number would lead to slowing down the bot. <br>*Defaults to `7`.* <br> ***Datatype:*** *Integer*
|
| `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy. <br> **Note** that it downloads historical data so increasing this number would lead to slowing down the bot. <br>*Defaults to `7`.* <br> ***Datatype:*** *Integer*
|
||||||
| `capital_available_percentage` | This is the percentage of the total capital on exchange in stake currency. <br>As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital. <br>*Defaults to `0.5`.* <br> ***Datatype:*** *Float*
|
| `capital_available_percentage` | **DEPRECATED - [replaced with `tradable_balance_ratio`](configuration.md#Available balance)** This is the percentage of the total capital on exchange in stake currency. <br>As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital. <br>*Defaults to `0.5`.* <br> ***Datatype:*** *Float*
|
||||||
| `allowed_risk` | Ratio of allowed risk per trade. <br>*Defaults to `0.01` (1%)).* <br> ***Datatype:*** *Float*
|
| `allowed_risk` | Ratio of allowed risk per trade. <br>*Defaults to `0.01` (1%)).* <br> ***Datatype:*** *Float*
|
||||||
| `stoploss_range_min` | Minimum stoploss. <br>*Defaults to `-0.01`.* <br> ***Datatype:*** *Float*
|
| `stoploss_range_min` | Minimum stoploss. <br>*Defaults to `-0.01`.* <br> ***Datatype:*** *Float*
|
||||||
| `stoploss_range_max` | Maximum stoploss. <br>*Defaults to `-0.10`.* <br> ***Datatype:*** *Float*
|
| `stoploss_range_max` | Maximum stoploss. <br>*Defaults to `-0.10`.* <br> ***Datatype:*** *Float*
|
||||||
|
@ -11,8 +11,10 @@
|
|||||||
<a class="github-button" href="https://github.com/freqtrade/freqtrade/archive/master.zip" data-icon="octicon-cloud-download" data-size="large" aria-label="Download freqtrade/freqtrade on GitHub">Download</a>
|
<a class="github-button" href="https://github.com/freqtrade/freqtrade/archive/master.zip" data-icon="octicon-cloud-download" data-size="large" aria-label="Download freqtrade/freqtrade on GitHub">Download</a>
|
||||||
<!-- Place this tag where you want the button to render. -->
|
<!-- Place this tag where you want the button to render. -->
|
||||||
<a class="github-button" href="https://github.com/freqtrade" data-size="large" aria-label="Follow @freqtrade on GitHub">Follow @freqtrade</a>
|
<a class="github-button" href="https://github.com/freqtrade" data-size="large" aria-label="Follow @freqtrade on GitHub">Follow @freqtrade</a>
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
Freqtrade is a cryptocurrency trading bot written in Python.
|
|
||||||
|
Freqtrade is a crypto-currency algorithmic trading software developed in python (3.6+) and supported on Windows, macOS and Linux.
|
||||||
|
|
||||||
!!! 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.
|
||||||
@ -23,18 +25,15 @@ Freqtrade is a cryptocurrency trading bot written in Python.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Based on Python 3.6+: For botting on any operating system — Windows, macOS and Linux.
|
- 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).
|
||||||
- Persistence: Persistence is achieved through sqlite database.
|
- Download market data: Download historical data of the exchange and the markets your may want to trade with.
|
||||||
- Dry-run mode: Run the bot without playing money.
|
- Backtest: Test your strategy on downloaded historical data.
|
||||||
- Backtesting: Run a simulation of your buy/sell strategy with historical data.
|
- Optimize: Find the best parameters for your strategy using hyperoptimization which employs machining learning methods. You can optimize buy, sell, take profit (ROI), stop-loss and trailing stop-loss parameters for your strategy.
|
||||||
- Strategy Optimization by machine learning: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
- 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.
|
||||||
- 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.
|
- Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode).
|
||||||
- Whitelist crypto-currencies: Select which crypto-currency you want to trade or use dynamic whitelists based on market (pair) trade volume.
|
- 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.
|
||||||
- Blacklist crypto-currencies: Select which crypto-currency you want to avoid.
|
- Control/Monitor: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.).
|
||||||
- Manageable via Telegram or REST APi: Manage the bot with Telegram or via the builtin REST API.
|
- 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).
|
||||||
- Display profit/loss in fiat: Display your profit/loss in any of 33 fiat currencies supported.
|
|
||||||
- Daily summary of profit/loss: Receive the daily summary of your profit/loss.
|
|
||||||
- Performance status report: Receive the performance status of your current trades.
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@ -61,10 +60,10 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
Help / Slack
|
### Help / Slack
|
||||||
For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel.
|
For any questions not covered by the documentation or for further information about the bot, we encourage you to join our passionate Slack community.
|
||||||
|
|
||||||
Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join Slack channel.
|
Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join the Freqtrade Slack channel.
|
||||||
|
|
||||||
## Ready to try?
|
## Ready to try?
|
||||||
|
|
||||||
|
@ -120,16 +120,77 @@ To plot trades from a backtesting result, use `--export-filename <filename>`
|
|||||||
freqtrade plot-dataframe --strategy AwesomeStrategy --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH
|
freqtrade plot-dataframe --strategy AwesomeStrategy --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Plot dataframe basics
|
||||||
|
|
||||||
|
![plot-dataframe2](assets/plot-dataframe2.png)
|
||||||
|
|
||||||
|
The `plot-dataframe` subcommand requires backtesting data, a strategy and either a backtesting-results file or a database, containing trades corresponding to the strategy.
|
||||||
|
|
||||||
|
The resulting plot will have the following elements:
|
||||||
|
|
||||||
|
* Green triangles: Buy signals from the strategy. (Note: not every buy signal generates a trade, compare to cyan circles.)
|
||||||
|
* Red triangles: Sell signals from the strategy. (Also, not every sell signal terminates a trade, compare to red and green squares.)
|
||||||
|
* Cyan circles: Trade entry points.
|
||||||
|
* Red squares: Trade exit points for trades with loss or 0% profit.
|
||||||
|
* Green squares: Trade exit points for profitable trades.
|
||||||
|
* Indicators with values corresponding to the candle scale (e.g. SMA/EMA), as specified with `--indicators1`.
|
||||||
|
* Volume (bar chart at the bottom of the main chart).
|
||||||
|
* Indicators with values in different scales (e.g. MACD, RSI) below the volume bars, as specified with `--indicators2`.
|
||||||
|
|
||||||
|
!!! Note "Bollinger Bands"
|
||||||
|
Bollinger bands are automatically added to the plot if the columns `bb_lowerband` and `bb_upperband` exist, and are painted as a light blue area spanning from the lower band to the upper band.
|
||||||
|
|
||||||
|
#### Advanced plot configuration
|
||||||
|
|
||||||
|
An advanced plot configuration can be specified in the strategy in the `plot_config` parameter.
|
||||||
|
|
||||||
|
Additional features when using plot_config include:
|
||||||
|
|
||||||
|
* Specify colors per indicator
|
||||||
|
* Specify additional subplots
|
||||||
|
|
||||||
|
The sample plot configuration below specifies fixed colors for the indicators. Otherwise consecutive plots may produce different colorschemes each time, making comparisons difficult.
|
||||||
|
It also allows multiple subplots to display both MACD and RSI at the same time.
|
||||||
|
|
||||||
|
Sample configuration with inline comments explaining the process:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
plot_config = {
|
||||||
|
'main_plot': {
|
||||||
|
# Configuration for main plot indicators.
|
||||||
|
# Specifies `ema10` to be red, and `ema50` to be a shade of gray
|
||||||
|
'ema10': {'color': 'red'},
|
||||||
|
'ema50': {'color': '#CCCCCC'},
|
||||||
|
# By omitting color, a random color is selected.
|
||||||
|
'sar': {},
|
||||||
|
},
|
||||||
|
'subplots': {
|
||||||
|
# Create subplot MACD
|
||||||
|
"MACD": {
|
||||||
|
'macd': {'color': 'blue'},
|
||||||
|
'macdsignal': {'color': 'orange'},
|
||||||
|
},
|
||||||
|
# Additional subplot RSI
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
The above configuration assumes that `ema10`, `ema50`, `macd`, `macdsignal` and `rsi` are columns in the DataFrame created by the strategy.
|
||||||
|
|
||||||
## Plot profit
|
## Plot profit
|
||||||
|
|
||||||
![plot-profit](assets/plot-profit.png)
|
![plot-profit](assets/plot-profit.png)
|
||||||
|
|
||||||
The `freqtrade plot-profit` subcommand shows an interactive graph with three plots:
|
The `plot-profit` subcommand shows an interactive graph with three plots:
|
||||||
|
|
||||||
1) Average closing price for all pairs
|
* Average closing price for all pairs.
|
||||||
2) The summarized profit made by backtesting.
|
* The summarized profit made by backtesting.
|
||||||
Note that this is not the real-world profit, but more of an estimate.
|
Note that this is not the real-world profit, but more of an estimate.
|
||||||
3) Profit for each individual pair
|
* Profit for each individual pair.
|
||||||
|
|
||||||
The first graph is good to get a grip of how the overall market progresses.
|
The first graph is good to get a grip of how the overall market progresses.
|
||||||
|
|
||||||
|
@ -387,15 +387,13 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"indicators1": Arg(
|
"indicators1": Arg(
|
||||||
'--indicators1',
|
'--indicators1',
|
||||||
help='Set indicators from your strategy you want in the first row of the graph. '
|
help='Set indicators from your strategy you want in the first row of the graph. '
|
||||||
'Space-separated list. Example: `ema3 ema5`. Default: `%(default)s`.',
|
"Space-separated list. Example: `ema3 ema5`. Default: `['sma', 'ema3', 'ema5']`.",
|
||||||
default=['sma', 'ema3', 'ema5'],
|
|
||||||
nargs='+',
|
nargs='+',
|
||||||
),
|
),
|
||||||
"indicators2": Arg(
|
"indicators2": Arg(
|
||||||
'--indicators2',
|
'--indicators2',
|
||||||
help='Set indicators from your strategy you want in the third row of the graph. '
|
help='Set indicators from your strategy you want in the third row of the graph. '
|
||||||
'Space-separated list. Example: `fastd fastk`. Default: `%(default)s`.',
|
"Space-separated list. Example: `fastd fastk`. Default: `['macd', 'macdsignal']`.",
|
||||||
default=['macd', 'macdsignal'],
|
|
||||||
nargs='+',
|
nargs='+',
|
||||||
),
|
),
|
||||||
"plot_limit": Arg(
|
"plot_limit": Arg(
|
||||||
|
@ -48,11 +48,6 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED
|
conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED
|
||||||
else:
|
else:
|
||||||
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED
|
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED
|
||||||
# Dynamically allow empty stake-currency
|
|
||||||
# Since the minimal config specifies this too.
|
|
||||||
# It's not allowed for Dry-run or live modes
|
|
||||||
conf_schema['properties']['stake_currency']['enum'] += [''] # type: ignore
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
FreqtradeValidator(conf_schema).validate(conf)
|
FreqtradeValidator(conf_schema).validate(conf)
|
||||||
return conf
|
return conf
|
||||||
@ -78,12 +73,24 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None:
|
|||||||
_validate_trailing_stoploss(conf)
|
_validate_trailing_stoploss(conf)
|
||||||
_validate_edge(conf)
|
_validate_edge(conf)
|
||||||
_validate_whitelist(conf)
|
_validate_whitelist(conf)
|
||||||
|
_validate_unlimited_amount(conf)
|
||||||
|
|
||||||
# validate configuration before returning
|
# validate configuration before returning
|
||||||
logger.info('Validating configuration ...')
|
logger.info('Validating configuration ...')
|
||||||
validate_config_schema(conf)
|
validate_config_schema(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_unlimited_amount(conf: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
If edge is disabled, either max_open_trades or stake_amount need to be set.
|
||||||
|
:raise: OperationalException if config validation failed
|
||||||
|
"""
|
||||||
|
if (not conf.get('edge', {}).get('enabled')
|
||||||
|
and conf.get('max_open_trades') == float('inf')
|
||||||
|
and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT):
|
||||||
|
raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.")
|
||||||
|
|
||||||
|
|
||||||
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
|
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
if conf.get('stoploss') == 0.0:
|
if conf.get('stoploss') == 0.0:
|
||||||
|
@ -80,3 +80,13 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
|||||||
f"Using precision_filter setting is deprecated and has been replaced by"
|
f"Using precision_filter setting is deprecated and has been replaced by"
|
||||||
"PrecisionFilter. Please refer to the docs on configuration details")
|
"PrecisionFilter. Please refer to the docs on configuration details")
|
||||||
config['pairlists'].append({'method': 'PrecisionFilter'})
|
config['pairlists'].append({'method': 'PrecisionFilter'})
|
||||||
|
|
||||||
|
if (config.get('edge', {}).get('enabled', False)
|
||||||
|
and 'capital_available_percentage' in config.get('edge', {})):
|
||||||
|
logger.warning(
|
||||||
|
"DEPRECATED: "
|
||||||
|
"Using 'edge.capital_available_percentage' has been deprecated in favor of "
|
||||||
|
"'tradable_balance_ratio'. Please migrate your configuration to "
|
||||||
|
"'tradable_balance_ratio' and remove 'capital_available_percentage' "
|
||||||
|
"from the edge configuration."
|
||||||
|
)
|
||||||
|
@ -35,12 +35,6 @@ USER_DATA_FILES = {
|
|||||||
'strategy_analysis_example.ipynb': 'notebooks',
|
'strategy_analysis_example.ipynb': 'notebooks',
|
||||||
}
|
}
|
||||||
|
|
||||||
TIMEFRAMES = [
|
|
||||||
'1m', '3m', '5m', '15m', '30m',
|
|
||||||
'1h', '2h', '4h', '6h', '8h', '12h',
|
|
||||||
'1d', '3d', '1w',
|
|
||||||
]
|
|
||||||
|
|
||||||
SUPPORTED_FIAT = [
|
SUPPORTED_FIAT = [
|
||||||
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
|
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
|
||||||
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
||||||
@ -68,13 +62,23 @@ CONF_SCHEMA = {
|
|||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1},
|
'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1},
|
||||||
'ticker_interval': {'type': 'string', 'enum': TIMEFRAMES},
|
'ticker_interval': {'type': 'string'},
|
||||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']},
|
'stake_currency': {'type': 'string'},
|
||||||
'stake_amount': {
|
'stake_amount': {
|
||||||
'type': ['number', 'string'],
|
'type': ['number', 'string'],
|
||||||
'minimum': 0.0001,
|
'minimum': 0.0001,
|
||||||
'pattern': UNLIMITED_STAKE_AMOUNT
|
'pattern': UNLIMITED_STAKE_AMOUNT
|
||||||
},
|
},
|
||||||
|
'tradable_balance_ratio': {
|
||||||
|
'type': 'number',
|
||||||
|
'minimum': 0.1,
|
||||||
|
'maximum': 1,
|
||||||
|
'default': 0.99
|
||||||
|
},
|
||||||
|
'amend_last_stake_amount': {'type': 'boolean', 'default': False},
|
||||||
|
'last_stake_amount_min_ratio': {
|
||||||
|
'type': 'number', 'minimum': 0.0, 'maximum': 1.0, 'default': 0.5
|
||||||
|
},
|
||||||
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
|
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
|
||||||
'dry_run': {'type': 'boolean'},
|
'dry_run': {'type': 'boolean'},
|
||||||
'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET},
|
'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET},
|
||||||
@ -279,7 +283,7 @@ CONF_SCHEMA = {
|
|||||||
'max_trade_duration_minute': {'type': 'integer'},
|
'max_trade_duration_minute': {'type': 'integer'},
|
||||||
'remove_pumps': {'type': 'boolean'}
|
'remove_pumps': {'type': 'boolean'}
|
||||||
},
|
},
|
||||||
'required': ['process_throttle_secs', 'allowed_risk', 'capital_available_percentage']
|
'required': ['process_throttle_secs', 'allowed_risk']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -289,6 +293,8 @@ SCHEMA_TRADE_REQUIRED = [
|
|||||||
'max_open_trades',
|
'max_open_trades',
|
||||||
'stake_currency',
|
'stake_currency',
|
||||||
'stake_amount',
|
'stake_amount',
|
||||||
|
'tradable_balance_ratio',
|
||||||
|
'last_stake_amount_min_ratio',
|
||||||
'dry_run',
|
'dry_run',
|
||||||
'dry_run_wallet',
|
'dry_run_wallet',
|
||||||
'bid_strategy',
|
'bid_strategy',
|
||||||
|
@ -57,7 +57,9 @@ class Edge:
|
|||||||
if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT:
|
if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT:
|
||||||
raise OperationalException('Edge works only with unlimited stake amount')
|
raise OperationalException('Edge works only with unlimited stake amount')
|
||||||
|
|
||||||
self._capital_percentage: float = self.edge_config.get('capital_available_percentage')
|
# Deprecated capital_available_percentage. Will use tradable_balance_ratio in the future.
|
||||||
|
self._capital_percentage: float = self.edge_config.get(
|
||||||
|
'capital_available_percentage', self.config['tradable_balance_ratio'])
|
||||||
self._allowed_risk: float = self.edge_config.get('allowed_risk')
|
self._allowed_risk: float = self.edge_config.get('allowed_risk')
|
||||||
self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14)
|
self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14)
|
||||||
self._last_updated: int = 0 # Timestamp of pairs last updated time
|
self._last_updated: int = 0 # Timestamp of pairs last updated time
|
||||||
|
@ -41,7 +41,7 @@ class Binance(Exchange):
|
|||||||
"""
|
"""
|
||||||
ordertype = "stop_loss_limit"
|
ordertype = "stop_loss_limit"
|
||||||
|
|
||||||
stop_price = self.symbol_price_prec(pair, stop_price)
|
stop_price = self.price_to_precision(pair, stop_price)
|
||||||
|
|
||||||
# Ensure rate is less than stop price
|
# Ensure rate is less than stop price
|
||||||
if stop_price <= rate:
|
if stop_price <= rate:
|
||||||
@ -57,9 +57,9 @@ class Binance(Exchange):
|
|||||||
params = self._params.copy()
|
params = self._params.copy()
|
||||||
params.update({'stopPrice': stop_price})
|
params.update({'stopPrice': stop_price})
|
||||||
|
|
||||||
amount = self.symbol_amount_prec(pair, amount)
|
amount = self.amount_to_precision(pair, amount)
|
||||||
|
|
||||||
rate = self.symbol_price_prec(pair, rate)
|
rate = self.price_to_precision(pair, rate)
|
||||||
|
|
||||||
order = self._api.create_order(pair, ordertype, 'sell',
|
order = self._api.create_order(pair, ordertype, 'sell',
|
||||||
amount, rate, params)
|
amount, rate, params)
|
||||||
|
@ -7,14 +7,15 @@ import inspect
|
|||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from math import ceil, floor
|
from math import ceil
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
import ccxt.async_support as ccxt_async
|
import ccxt.async_support as ccxt_async
|
||||||
from ccxt.base.decimal_to_precision import ROUND_DOWN, ROUND_UP
|
from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE,
|
||||||
|
TRUNCATE, decimal_to_precision)
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
@ -116,6 +117,7 @@ class Exchange:
|
|||||||
self._load_markets()
|
self._load_markets()
|
||||||
|
|
||||||
# Check if all pairs are available
|
# Check if all pairs are available
|
||||||
|
self.validate_stakecurrency(config['stake_currency'])
|
||||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||||
self.validate_ordertypes(config.get('order_types', {}))
|
self.validate_ordertypes(config.get('order_types', {}))
|
||||||
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
||||||
@ -188,6 +190,11 @@ class Exchange:
|
|||||||
self._load_markets()
|
self._load_markets()
|
||||||
return self._api.markets
|
return self._api.markets
|
||||||
|
|
||||||
|
@property
|
||||||
|
def precisionMode(self) -> str:
|
||||||
|
"""exchange ccxt precisionMode"""
|
||||||
|
return self._api.precisionMode
|
||||||
|
|
||||||
def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
|
def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
|
||||||
pairs_only: bool = False, active_only: bool = False) -> Dict:
|
pairs_only: bool = False, active_only: bool = False) -> Dict:
|
||||||
"""
|
"""
|
||||||
@ -210,6 +217,13 @@ class Exchange:
|
|||||||
markets = {k: v for k, v in markets.items() if market_is_active(v)}
|
markets = {k: v for k, v in markets.items() if market_is_active(v)}
|
||||||
return markets
|
return markets
|
||||||
|
|
||||||
|
def get_quote_currencies(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Return a list of supported quote currencies
|
||||||
|
"""
|
||||||
|
markets = self.markets
|
||||||
|
return sorted(set([x['quote'] for _, x in markets.items()]))
|
||||||
|
|
||||||
def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame:
|
def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame:
|
||||||
if pair_interval in self._klines:
|
if pair_interval in self._klines:
|
||||||
return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
|
return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
|
||||||
@ -259,11 +273,23 @@ class Exchange:
|
|||||||
except ccxt.BaseError:
|
except ccxt.BaseError:
|
||||||
logger.exception("Could not reload markets.")
|
logger.exception("Could not reload markets.")
|
||||||
|
|
||||||
|
def validate_stakecurrency(self, stake_currency) -> None:
|
||||||
|
"""
|
||||||
|
Checks stake-currency against available currencies on the exchange.
|
||||||
|
:param stake_currency: Stake-currency to validate
|
||||||
|
:raise: OperationalException if stake-currency is not available.
|
||||||
|
"""
|
||||||
|
quote_currencies = self.get_quote_currencies()
|
||||||
|
if stake_currency not in quote_currencies:
|
||||||
|
raise OperationalException(
|
||||||
|
f"{stake_currency} is not available as stake on {self.name}. "
|
||||||
|
f"Available currencies are: {', '.join(quote_currencies)}")
|
||||||
|
|
||||||
def validate_pairs(self, pairs: List[str]) -> None:
|
def validate_pairs(self, pairs: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
Checks if all given pairs are tradable on the current exchange.
|
Checks if all given pairs are tradable on the current exchange.
|
||||||
Raises OperationalException if one pair is not available.
|
|
||||||
:param pairs: list of pairs
|
:param pairs: list of pairs
|
||||||
|
:raise: OperationalException if one pair is not available
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -319,6 +345,10 @@ class Exchange:
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}")
|
f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}")
|
||||||
|
|
||||||
|
if timeframe and timeframe_to_minutes(timeframe) < 1:
|
||||||
|
raise OperationalException(
|
||||||
|
f"Timeframes < 1m are currently not supported by Freqtrade.")
|
||||||
|
|
||||||
def validate_ordertypes(self, order_types: Dict) -> None:
|
def validate_ordertypes(self, order_types: Dict) -> None:
|
||||||
"""
|
"""
|
||||||
Checks if order-types configured in strategy/config are supported
|
Checks if order-types configured in strategy/config are supported
|
||||||
@ -362,32 +392,49 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
return endpoint in self._api.has and self._api.has[endpoint]
|
return endpoint in self._api.has and self._api.has[endpoint]
|
||||||
|
|
||||||
def symbol_amount_prec(self, pair, amount: float):
|
def amount_to_precision(self, pair, amount: float) -> float:
|
||||||
'''
|
'''
|
||||||
Returns the amount to buy or sell to a precision the Exchange accepts
|
Returns the amount to buy or sell to a precision the Exchange accepts
|
||||||
Rounded down
|
Reimplementation of ccxt internal methods - ensuring we can test the result is correct
|
||||||
|
based on our definitions.
|
||||||
'''
|
'''
|
||||||
if self.markets[pair]['precision']['amount']:
|
if self.markets[pair]['precision']['amount']:
|
||||||
symbol_prec = self.markets[pair]['precision']['amount']
|
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
|
||||||
big_amount = amount * pow(10, symbol_prec)
|
precision=self.markets[pair]['precision']['amount'],
|
||||||
amount = floor(big_amount) / pow(10, symbol_prec)
|
counting_mode=self.precisionMode,
|
||||||
|
))
|
||||||
|
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
def symbol_price_prec(self, pair, price: float):
|
def price_to_precision(self, pair, price: float) -> float:
|
||||||
'''
|
'''
|
||||||
Returns the price buying or selling with to the precision the Exchange accepts
|
Returns the price rounded up to the precision the Exchange accepts.
|
||||||
|
Partial Reimplementation of ccxt internal method decimal_to_precision(),
|
||||||
|
which does not support rounding up
|
||||||
|
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
|
||||||
|
align with amount_to_precision().
|
||||||
Rounds up
|
Rounds up
|
||||||
'''
|
'''
|
||||||
if self.markets[pair]['precision']['price']:
|
if self.markets[pair]['precision']['price']:
|
||||||
symbol_prec = self.markets[pair]['precision']['price']
|
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
|
||||||
big_price = price * pow(10, symbol_prec)
|
# precision=self.markets[pair]['precision']['price'],
|
||||||
price = ceil(big_price) / pow(10, symbol_prec)
|
# counting_mode=self.precisionMode,
|
||||||
|
# ))
|
||||||
|
if self.precisionMode == TICK_SIZE:
|
||||||
|
precision = self.markets[pair]['precision']['price']
|
||||||
|
missing = price % precision
|
||||||
|
if missing != 0:
|
||||||
|
price = price - missing + precision
|
||||||
|
else:
|
||||||
|
symbol_prec = self.markets[pair]['precision']['price']
|
||||||
|
big_price = price * pow(10, symbol_prec)
|
||||||
|
price = ceil(big_price) / pow(10, symbol_prec)
|
||||||
return price
|
return price
|
||||||
|
|
||||||
def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||||
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
||||||
order_id = f'dry_run_{side}_{randint(0, 10**6)}'
|
order_id = f'dry_run_{side}_{randint(0, 10**6)}'
|
||||||
_amount = self.symbol_amount_prec(pair, amount)
|
_amount = self.amount_to_precision(pair, amount)
|
||||||
dry_order = {
|
dry_order = {
|
||||||
"id": order_id,
|
"id": order_id,
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
@ -422,13 +469,13 @@ class Exchange:
|
|||||||
rate: float, params: Dict = {}) -> Dict:
|
rate: float, params: Dict = {}) -> Dict:
|
||||||
try:
|
try:
|
||||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
amount = self.symbol_amount_prec(pair, amount)
|
amount = self.amount_to_precision(pair, amount)
|
||||||
needs_price = (ordertype != 'market'
|
needs_price = (ordertype != 'market'
|
||||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
||||||
rate = self.symbol_price_prec(pair, rate) if needs_price else None
|
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||||
|
|
||||||
return self._api.create_order(pair, ordertype, side,
|
return self._api.create_order(pair, ordertype, side,
|
||||||
amount, rate, params)
|
amount, rate_for_order, params)
|
||||||
|
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
|
@ -63,8 +63,7 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||||
|
|
||||||
persistence.init(self.config.get('db_url', None),
|
persistence.init(self.config.get('db_url', None), clean_open_orders=self.config['dry_run'])
|
||||||
clean_open_orders=self.config.get('dry_run', False))
|
|
||||||
|
|
||||||
self.wallets = Wallets(self.config, self.exchange)
|
self.wallets = Wallets(self.config, self.exchange)
|
||||||
|
|
||||||
@ -250,12 +249,16 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
return used_rate
|
return used_rate
|
||||||
|
|
||||||
def get_trade_stake_amount(self, pair) -> Optional[float]:
|
def get_trade_stake_amount(self, pair) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate stake amount for the trade
|
Calculate stake amount for the trade
|
||||||
:return: float: Stake amount
|
:return: float: Stake amount
|
||||||
|
:raise: DependencyException if the available stake amount is too low
|
||||||
"""
|
"""
|
||||||
stake_amount: Optional[float]
|
stake_amount: float
|
||||||
|
# Ensure wallets are uptodate.
|
||||||
|
self.wallets.update()
|
||||||
|
|
||||||
if self.edge:
|
if self.edge:
|
||||||
stake_amount = self.edge.stake_amount(
|
stake_amount = self.edge.stake_amount(
|
||||||
pair,
|
pair,
|
||||||
@ -270,26 +273,52 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
return self._check_available_stake_amount(stake_amount)
|
return self._check_available_stake_amount(stake_amount)
|
||||||
|
|
||||||
def _calculate_unlimited_stake_amount(self) -> Optional[float]:
|
def _get_available_stake_amount(self) -> float:
|
||||||
|
"""
|
||||||
|
Return the total currently available balance in stake currency,
|
||||||
|
respecting tradable_balance_ratio.
|
||||||
|
Calculated as
|
||||||
|
<open_trade stakes> + free amount ) * tradable_balance_ratio - <open_trade stakes>
|
||||||
|
"""
|
||||||
|
val_tied_up = Trade.total_open_trades_stakes()
|
||||||
|
|
||||||
|
# Ensure <tradable_balance_ratio>% is used from the overall balance
|
||||||
|
# Otherwise we'd risk lowering stakes with each open trade.
|
||||||
|
# (tied up + current free) * ratio) - tied up
|
||||||
|
available_amount = ((val_tied_up + self.wallets.get_free(self.config['stake_currency'])) *
|
||||||
|
self.config['tradable_balance_ratio']) - val_tied_up
|
||||||
|
return available_amount
|
||||||
|
|
||||||
|
def _calculate_unlimited_stake_amount(self) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate stake amount for "unlimited" stake amount
|
Calculate stake amount for "unlimited" stake amount
|
||||||
:return: None if max number of trades reached
|
:return: 0 if max number of trades reached, else stake_amount to use.
|
||||||
"""
|
"""
|
||||||
free_open_trades = self.get_free_open_trades()
|
free_open_trades = self.get_free_open_trades()
|
||||||
if not free_open_trades:
|
if not free_open_trades:
|
||||||
return None
|
return 0
|
||||||
available_amount = self.wallets.get_free(self.config['stake_currency'])
|
|
||||||
|
available_amount = self._get_available_stake_amount()
|
||||||
|
|
||||||
return available_amount / free_open_trades
|
return available_amount / free_open_trades
|
||||||
|
|
||||||
def _check_available_stake_amount(self, stake_amount: Optional[float]) -> Optional[float]:
|
def _check_available_stake_amount(self, stake_amount: float) -> float:
|
||||||
"""
|
"""
|
||||||
Check if stake amount can be fulfilled with the available balance
|
Check if stake amount can be fulfilled with the available balance
|
||||||
for the stake currency
|
for the stake currency
|
||||||
:return: float: Stake amount
|
:return: float: Stake amount
|
||||||
"""
|
"""
|
||||||
available_amount = self.wallets.get_free(self.config['stake_currency'])
|
available_amount = self._get_available_stake_amount()
|
||||||
|
|
||||||
if stake_amount is not None and available_amount < stake_amount:
|
if self.config['amend_last_stake_amount']:
|
||||||
|
# Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio
|
||||||
|
# Otherwise the remaining amount is too low to trade.
|
||||||
|
if available_amount > (stake_amount * self.config['last_stake_amount_min_ratio']):
|
||||||
|
stake_amount = min(stake_amount, available_amount)
|
||||||
|
else:
|
||||||
|
stake_amount = 0
|
||||||
|
|
||||||
|
if available_amount < stake_amount:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f"Available balance ({available_amount} {self.config['stake_currency']}) is "
|
f"Available balance ({available_amount} {self.config['stake_currency']}) is "
|
||||||
f"lower than stake amount ({stake_amount} {self.config['stake_currency']})"
|
f"lower than stake amount ({stake_amount} {self.config['stake_currency']})"
|
||||||
@ -872,15 +901,19 @@ class FreqtradeBot:
|
|||||||
:return: amount to sell
|
:return: amount to sell
|
||||||
:raise: DependencyException: if available balance is not within 2% of the available amount.
|
:raise: DependencyException: if available balance is not within 2% of the available amount.
|
||||||
"""
|
"""
|
||||||
|
# Update wallets to ensure amounts tied up in a stoploss is now free!
|
||||||
|
self.wallets.update()
|
||||||
|
|
||||||
wallet_amount = self.wallets.get_free(pair.split('/')[0])
|
wallet_amount = self.wallets.get_free(pair.split('/')[0])
|
||||||
logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}")
|
logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}")
|
||||||
if wallet_amount > amount:
|
if wallet_amount >= amount:
|
||||||
return amount
|
return amount
|
||||||
elif wallet_amount > amount * 0.98:
|
elif wallet_amount > amount * 0.98:
|
||||||
logger.info(f"{pair} - Falling back to wallet-amount.")
|
logger.info(f"{pair} - Falling back to wallet-amount.")
|
||||||
return wallet_amount
|
return wallet_amount
|
||||||
else:
|
else:
|
||||||
raise DependencyException("Not enough amount to sell.")
|
raise DependencyException(
|
||||||
|
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
||||||
|
|
||||||
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None:
|
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None:
|
||||||
"""
|
"""
|
||||||
@ -896,7 +929,7 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
# if stoploss is on exchange and we are on dry_run mode,
|
# if stoploss is on exchange and we are on dry_run mode,
|
||||||
# we consider the sell price stop price
|
# we consider the sell price stop price
|
||||||
if self.config.get('dry_run', False) and sell_type == 'stoploss' \
|
if self.config['dry_run'] and sell_type == 'stoploss' \
|
||||||
and self.strategy.order_types['stoploss_on_exchange']:
|
and self.strategy.order_types['stoploss_on_exchange']:
|
||||||
limit = trade.stop_loss
|
limit = trade.stop_loss
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, List, NamedTuple, Optional
|
from typing import Any, Dict, List, NamedTuple, Optional
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from tabulate import tabulate
|
|
||||||
|
|
||||||
from freqtrade.configuration import (TimeRange, remove_credentials,
|
from freqtrade.configuration import (TimeRange, remove_credentials,
|
||||||
validate_config_consistency)
|
validate_config_consistency)
|
||||||
@ -20,6 +19,9 @@ from freqtrade.data.dataprovider import DataProvider
|
|||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||||
from freqtrade.misc import file_dump_json
|
from freqtrade.misc import file_dump_json
|
||||||
|
from freqtrade.optimize.optimize_reports import (
|
||||||
|
generate_text_table, generate_text_table_sell_reason,
|
||||||
|
generate_text_table_strategy)
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
@ -131,96 +133,6 @@ class Backtesting:
|
|||||||
|
|
||||||
return data, timerange
|
return data, timerange
|
||||||
|
|
||||||
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame,
|
|
||||||
skip_nan: bool = False) -> str:
|
|
||||||
"""
|
|
||||||
Generates and returns a text table for the given backtest data and the results dataframe
|
|
||||||
:return: pretty printed table with tabulate as str
|
|
||||||
"""
|
|
||||||
stake_currency = str(self.config.get('stake_currency'))
|
|
||||||
max_open_trades = self.config.get('max_open_trades')
|
|
||||||
|
|
||||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
|
||||||
tabular_data = []
|
|
||||||
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
|
|
||||||
'tot profit ' + stake_currency, 'tot profit %', 'avg duration',
|
|
||||||
'profit', 'loss']
|
|
||||||
for pair in data:
|
|
||||||
result = results[results.pair == pair]
|
|
||||||
if skip_nan and result.profit_abs.isnull().all():
|
|
||||||
continue
|
|
||||||
|
|
||||||
tabular_data.append([
|
|
||||||
pair,
|
|
||||||
len(result.index),
|
|
||||||
result.profit_percent.mean() * 100.0,
|
|
||||||
result.profit_percent.sum() * 100.0,
|
|
||||||
result.profit_abs.sum(),
|
|
||||||
result.profit_percent.sum() * 100.0 / max_open_trades,
|
|
||||||
str(timedelta(
|
|
||||||
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
|
|
||||||
len(result[result.profit_abs > 0]),
|
|
||||||
len(result[result.profit_abs < 0])
|
|
||||||
])
|
|
||||||
|
|
||||||
# Append Total
|
|
||||||
tabular_data.append([
|
|
||||||
'TOTAL',
|
|
||||||
len(results.index),
|
|
||||||
results.profit_percent.mean() * 100.0,
|
|
||||||
results.profit_percent.sum() * 100.0,
|
|
||||||
results.profit_abs.sum(),
|
|
||||||
results.profit_percent.sum() * 100.0 / max_open_trades,
|
|
||||||
str(timedelta(
|
|
||||||
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
|
||||||
len(results[results.profit_abs > 0]),
|
|
||||||
len(results[results.profit_abs < 0])
|
|
||||||
])
|
|
||||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
|
||||||
return tabulate(tabular_data, headers=headers,
|
|
||||||
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
|
||||||
|
|
||||||
def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
|
|
||||||
"""
|
|
||||||
Generate small table outlining Backtest results
|
|
||||||
"""
|
|
||||||
tabular_data = []
|
|
||||||
headers = ['Sell Reason', 'Count', 'Profit', 'Loss']
|
|
||||||
for reason, count in results['sell_reason'].value_counts().iteritems():
|
|
||||||
profit = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] >= 0)])
|
|
||||||
loss = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] < 0)])
|
|
||||||
tabular_data.append([reason.value, count, profit, loss])
|
|
||||||
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
|
|
||||||
|
|
||||||
def _generate_text_table_strategy(self, all_results: dict) -> str:
|
|
||||||
"""
|
|
||||||
Generate summary table per strategy
|
|
||||||
"""
|
|
||||||
stake_currency = str(self.config.get('stake_currency'))
|
|
||||||
max_open_trades = self.config.get('max_open_trades')
|
|
||||||
|
|
||||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
|
||||||
tabular_data = []
|
|
||||||
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
|
|
||||||
'tot profit ' + stake_currency, 'tot profit %', 'avg duration',
|
|
||||||
'profit', 'loss']
|
|
||||||
for strategy, results in all_results.items():
|
|
||||||
tabular_data.append([
|
|
||||||
strategy,
|
|
||||||
len(results.index),
|
|
||||||
results.profit_percent.mean() * 100.0,
|
|
||||||
results.profit_percent.sum() * 100.0,
|
|
||||||
results.profit_abs.sum(),
|
|
||||||
results.profit_percent.sum() * 100.0 / max_open_trades,
|
|
||||||
str(timedelta(
|
|
||||||
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
|
||||||
len(results[results.profit_abs > 0]),
|
|
||||||
len(results[results.profit_abs < 0])
|
|
||||||
])
|
|
||||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
|
||||||
return tabulate(tabular_data, headers=headers,
|
|
||||||
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
|
||||||
|
|
||||||
def _store_backtest_result(self, recordfilename: Path, results: DataFrame,
|
def _store_backtest_result(self, recordfilename: Path, results: DataFrame,
|
||||||
strategyname: Optional[str] = None) -> None:
|
strategyname: Optional[str] = None) -> None:
|
||||||
|
|
||||||
@ -386,7 +298,7 @@ class Backtesting:
|
|||||||
"""
|
"""
|
||||||
# Arguments are long and noisy, so this is commented out.
|
# Arguments are long and noisy, so this is commented out.
|
||||||
# Uncomment if you need to debug the backtest() method.
|
# Uncomment if you need to debug the backtest() method.
|
||||||
# logger.debug(f"Start backtest, args: {args}")
|
# logger.debug(f"Start backtest, args: {args}")
|
||||||
processed = args['processed']
|
processed = args['processed']
|
||||||
stake_amount = args['stake_amount']
|
stake_amount = args['stake_amount']
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
@ -511,16 +423,24 @@ class Backtesting:
|
|||||||
|
|
||||||
print(f"Result for strategy {strategy}")
|
print(f"Result for strategy {strategy}")
|
||||||
print(' BACKTESTING REPORT '.center(133, '='))
|
print(' BACKTESTING REPORT '.center(133, '='))
|
||||||
print(self._generate_text_table(data, results))
|
print(generate_text_table(data,
|
||||||
|
stake_currency=self.config['stake_currency'],
|
||||||
|
max_open_trades=self.config['max_open_trades'],
|
||||||
|
results=results))
|
||||||
|
|
||||||
print(' SELL REASON STATS '.center(133, '='))
|
print(' SELL REASON STATS '.center(133, '='))
|
||||||
print(self._generate_text_table_sell_reason(data, results))
|
print(generate_text_table_sell_reason(data, results))
|
||||||
|
|
||||||
print(' LEFT OPEN TRADES REPORT '.center(133, '='))
|
print(' LEFT OPEN TRADES REPORT '.center(133, '='))
|
||||||
print(self._generate_text_table(data, results.loc[results.open_at_end], True))
|
print(generate_text_table(data,
|
||||||
|
stake_currency=self.config['stake_currency'],
|
||||||
|
max_open_trades=self.config['max_open_trades'],
|
||||||
|
results=results.loc[results.open_at_end], skip_nan=True))
|
||||||
print()
|
print()
|
||||||
if len(all_results) > 1:
|
if len(all_results) > 1:
|
||||||
# Print Strategy summary table
|
# Print Strategy summary table
|
||||||
print(' Strategy Summary '.center(133, '='))
|
print(' Strategy Summary '.center(133, '='))
|
||||||
print(self._generate_text_table_strategy(all_results))
|
print(generate_text_table_strategy(self.config['stake_currency'],
|
||||||
|
self.config['max_open_trades'],
|
||||||
|
all_results=all_results))
|
||||||
print('\nFor more details, please look at the detail tables above')
|
print('\nFor more details, please look at the detail tables above')
|
||||||
|
@ -6,13 +6,12 @@ This module contains the edge backtesting interface
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from tabulate import tabulate
|
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.configuration import (TimeRange, remove_credentials,
|
from freqtrade.configuration import (TimeRange, remove_credentials,
|
||||||
validate_config_consistency)
|
validate_config_consistency)
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.resolvers import StrategyResolver, ExchangeResolver
|
from freqtrade.optimize.optimize_reports import generate_edge_table
|
||||||
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -44,33 +43,8 @@ class EdgeCli:
|
|||||||
self.edge._timerange = TimeRange.parse_timerange(None if self.config.get(
|
self.edge._timerange = TimeRange.parse_timerange(None if self.config.get(
|
||||||
'timerange') is None else str(self.config.get('timerange')))
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
|
|
||||||
def _generate_edge_table(self, results: dict) -> str:
|
|
||||||
|
|
||||||
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d')
|
|
||||||
tabular_data = []
|
|
||||||
headers = ['pair', 'stoploss', 'win rate', 'risk reward ratio',
|
|
||||||
'required risk reward', 'expectancy', 'total number of trades',
|
|
||||||
'average duration (min)']
|
|
||||||
|
|
||||||
for result in results.items():
|
|
||||||
if result[1].nb_trades > 0:
|
|
||||||
tabular_data.append([
|
|
||||||
result[0],
|
|
||||||
result[1].stoploss,
|
|
||||||
result[1].winrate,
|
|
||||||
result[1].risk_reward_ratio,
|
|
||||||
result[1].required_risk_reward,
|
|
||||||
result[1].expectancy,
|
|
||||||
result[1].nb_trades,
|
|
||||||
round(result[1].avg_trade_duration)
|
|
||||||
])
|
|
||||||
|
|
||||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
|
||||||
return tabulate(tabular_data, headers=headers,
|
|
||||||
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
result = self.edge.calculate()
|
result = self.edge.calculate()
|
||||||
if result:
|
if result:
|
||||||
print('') # blank line for readability
|
print('') # blank line for readability
|
||||||
print(self._generate_edge_table(self.edge._cached_pairs))
|
print(generate_edge_table(self.edge._cached_pairs))
|
||||||
|
135
freqtrade/optimize/optimize_reports.py
Normal file
135
freqtrade/optimize/optimize_reports.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
from tabulate import tabulate
|
||||||
|
|
||||||
|
|
||||||
|
def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int,
|
||||||
|
results: DataFrame, skip_nan: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
|
:param data: Dict of <pair: dataframe> containing data that was used during backtesting.
|
||||||
|
:param stake_currency: stake-currency - used to correctly name headers
|
||||||
|
:param max_open_trades: Maximum allowed open trades
|
||||||
|
:param results: Dataframe containing the backtest results
|
||||||
|
:param skip_nan: Print "left open" open trades
|
||||||
|
:return: pretty printed table with tabulate as string
|
||||||
|
"""
|
||||||
|
|
||||||
|
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||||
|
tabular_data = []
|
||||||
|
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
|
||||||
|
f'tot profit {stake_currency}', 'tot profit %', 'avg duration',
|
||||||
|
'profit', 'loss']
|
||||||
|
for pair in data:
|
||||||
|
result = results[results.pair == pair]
|
||||||
|
if skip_nan and result.profit_abs.isnull().all():
|
||||||
|
continue
|
||||||
|
|
||||||
|
tabular_data.append([
|
||||||
|
pair,
|
||||||
|
len(result.index),
|
||||||
|
result.profit_percent.mean() * 100.0,
|
||||||
|
result.profit_percent.sum() * 100.0,
|
||||||
|
result.profit_abs.sum(),
|
||||||
|
result.profit_percent.sum() * 100.0 / max_open_trades,
|
||||||
|
str(timedelta(
|
||||||
|
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
|
||||||
|
len(result[result.profit_abs > 0]),
|
||||||
|
len(result[result.profit_abs < 0])
|
||||||
|
])
|
||||||
|
|
||||||
|
# Append Total
|
||||||
|
tabular_data.append([
|
||||||
|
'TOTAL',
|
||||||
|
len(results.index),
|
||||||
|
results.profit_percent.mean() * 100.0,
|
||||||
|
results.profit_percent.sum() * 100.0,
|
||||||
|
results.profit_abs.sum(),
|
||||||
|
results.profit_percent.sum() * 100.0 / max_open_trades,
|
||||||
|
str(timedelta(
|
||||||
|
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
||||||
|
len(results[results.profit_abs > 0]),
|
||||||
|
len(results[results.profit_abs < 0])
|
||||||
|
])
|
||||||
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
|
return tabulate(tabular_data, headers=headers,
|
||||||
|
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -> str:
|
||||||
|
"""
|
||||||
|
Generate small table outlining Backtest results
|
||||||
|
:param data: Dict of <pair: dataframe> containing data that was used during backtesting.
|
||||||
|
:param results: Dataframe containing the backtest results
|
||||||
|
:return: pretty printed table with tabulate as string
|
||||||
|
"""
|
||||||
|
tabular_data = []
|
||||||
|
headers = ['Sell Reason', 'Count', 'Profit', 'Loss', 'Profit %']
|
||||||
|
for reason, count in results['sell_reason'].value_counts().iteritems():
|
||||||
|
result = results.loc[results['sell_reason'] == reason]
|
||||||
|
profit = len(result[result['profit_abs'] >= 0])
|
||||||
|
loss = len(result[results['profit_abs'] < 0])
|
||||||
|
profit_mean = round(result['profit_percent'].mean() * 100.0, 2)
|
||||||
|
tabular_data.append([reason.value, count, profit, loss, profit_mean])
|
||||||
|
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_text_table_strategy(stake_currency: str, max_open_trades: str,
|
||||||
|
all_results: Dict) -> str:
|
||||||
|
"""
|
||||||
|
Generate summary table per strategy
|
||||||
|
:param stake_currency: stake-currency - used to correctly name headers
|
||||||
|
:param max_open_trades: Maximum allowed open trades used for backtest
|
||||||
|
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
|
||||||
|
:return: pretty printed table with tabulate as string
|
||||||
|
"""
|
||||||
|
|
||||||
|
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||||
|
tabular_data = []
|
||||||
|
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
|
||||||
|
f'tot profit {stake_currency}', 'tot profit %', 'avg duration',
|
||||||
|
'profit', 'loss']
|
||||||
|
for strategy, results in all_results.items():
|
||||||
|
tabular_data.append([
|
||||||
|
strategy,
|
||||||
|
len(results.index),
|
||||||
|
results.profit_percent.mean() * 100.0,
|
||||||
|
results.profit_percent.sum() * 100.0,
|
||||||
|
results.profit_abs.sum(),
|
||||||
|
results.profit_percent.sum() * 100.0 / max_open_trades,
|
||||||
|
str(timedelta(
|
||||||
|
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
||||||
|
len(results[results.profit_abs > 0]),
|
||||||
|
len(results[results.profit_abs < 0])
|
||||||
|
])
|
||||||
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
|
return tabulate(tabular_data, headers=headers,
|
||||||
|
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def generate_edge_table(results: dict) -> str:
|
||||||
|
|
||||||
|
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d')
|
||||||
|
tabular_data = []
|
||||||
|
headers = ['pair', 'stoploss', 'win rate', 'risk reward ratio',
|
||||||
|
'required risk reward', 'expectancy', 'total number of trades',
|
||||||
|
'average duration (min)']
|
||||||
|
|
||||||
|
for result in results.items():
|
||||||
|
if result[1].nb_trades > 0:
|
||||||
|
tabular_data.append([
|
||||||
|
result[0],
|
||||||
|
result[1].stoploss,
|
||||||
|
result[1].winrate,
|
||||||
|
result[1].risk_reward_ratio,
|
||||||
|
result[1].required_risk_reward,
|
||||||
|
result[1].expectancy,
|
||||||
|
result[1].nb_trades,
|
||||||
|
round(result[1].avg_trade_duration)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
|
return tabulate(tabular_data, headers=headers,
|
||||||
|
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
@ -35,8 +35,8 @@ class PrecisionFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
stop_price = ticker['ask'] * stoploss
|
stop_price = ticker['ask'] * stoploss
|
||||||
# Adjust stop-prices to precision
|
# Adjust stop-prices to precision
|
||||||
sp = self._exchange.symbol_price_prec(ticker["symbol"], stop_price)
|
sp = self._exchange.price_to_precision(ticker["symbol"], stop_price)
|
||||||
stop_gap_price = self._exchange.symbol_price_prec(ticker["symbol"], stop_price * 0.99)
|
stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99)
|
||||||
logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}")
|
logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}")
|
||||||
if sp <= stop_gap_price:
|
if sp <= stop_gap_price:
|
||||||
logger.info(f"Removed {ticker['symbol']} from whitelist, "
|
logger.info(f"Removed {ticker['symbol']} from whitelist, "
|
||||||
|
@ -58,21 +58,27 @@ def init_plotscript(config):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> make_subplots:
|
def add_indicators(fig, row, indicators: Dict[str, Dict], data: pd.DataFrame) -> make_subplots:
|
||||||
"""
|
"""
|
||||||
Generator all the indicator selected by the user for a specific row
|
Generate all the indicators selected by the user for a specific row, based on the configuration
|
||||||
:param fig: Plot figure to append to
|
:param fig: Plot figure to append to
|
||||||
:param row: row number for this plot
|
:param row: row number for this plot
|
||||||
:param indicators: List of indicators present in the dataframe
|
:param indicators: Dict of Indicators with configuration options.
|
||||||
|
Dict key must correspond to dataframe column.
|
||||||
:param data: candlestick DataFrame
|
:param data: candlestick DataFrame
|
||||||
"""
|
"""
|
||||||
for indicator in indicators:
|
for indicator, conf in indicators.items():
|
||||||
|
logger.debug(f"indicator {indicator} with config {conf}")
|
||||||
if indicator in data:
|
if indicator in data:
|
||||||
|
kwargs = {'x': data['date'],
|
||||||
|
'y': data[indicator].values,
|
||||||
|
'mode': 'lines',
|
||||||
|
'name': indicator
|
||||||
|
}
|
||||||
|
if 'color' in conf:
|
||||||
|
kwargs.update({'line': {'color': conf['color']}})
|
||||||
scatter = go.Scatter(
|
scatter = go.Scatter(
|
||||||
x=data['date'],
|
**kwargs
|
||||||
y=data[indicator].values,
|
|
||||||
mode='lines',
|
|
||||||
name=indicator
|
|
||||||
)
|
)
|
||||||
fig.add_trace(scatter, row, 1)
|
fig.add_trace(scatter, row, 1)
|
||||||
else:
|
else:
|
||||||
@ -111,11 +117,31 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
"""
|
"""
|
||||||
# Trades can be empty
|
# Trades can be empty
|
||||||
if trades is not None and len(trades) > 0:
|
if trades is not None and len(trades) > 0:
|
||||||
|
# Create description for sell summarizing the trade
|
||||||
|
trades['desc'] = trades.apply(lambda row: f"{round(row['profitperc'] * 100, 1)}%, "
|
||||||
|
f"{row['sell_reason']}, {row['duration']} min",
|
||||||
|
axis=1)
|
||||||
trade_buys = go.Scatter(
|
trade_buys = go.Scatter(
|
||||||
x=trades["open_time"],
|
x=trades["open_time"],
|
||||||
y=trades["open_rate"],
|
y=trades["open_rate"],
|
||||||
mode='markers',
|
mode='markers',
|
||||||
name='trade_buy',
|
name='Trade buy',
|
||||||
|
text=trades["desc"],
|
||||||
|
marker=dict(
|
||||||
|
symbol='circle-open',
|
||||||
|
size=11,
|
||||||
|
line=dict(width=2),
|
||||||
|
color='cyan'
|
||||||
|
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
trade_sells = go.Scatter(
|
||||||
|
x=trades.loc[trades['profitperc'] > 0, "close_time"],
|
||||||
|
y=trades.loc[trades['profitperc'] > 0, "close_rate"],
|
||||||
|
text=trades.loc[trades['profitperc'] > 0, "desc"],
|
||||||
|
mode='markers',
|
||||||
|
name='Sell - Profit',
|
||||||
marker=dict(
|
marker=dict(
|
||||||
symbol='square-open',
|
symbol='square-open',
|
||||||
size=11,
|
size=11,
|
||||||
@ -123,16 +149,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
color='green'
|
color='green'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Create description for sell summarizing the trade
|
trade_sells_loss = go.Scatter(
|
||||||
desc = trades.apply(lambda row: f"{round(row['profitperc'] * 100, 1)}%, "
|
x=trades.loc[trades['profitperc'] <= 0, "close_time"],
|
||||||
f"{row['sell_reason']}, {row['duration']} min",
|
y=trades.loc[trades['profitperc'] <= 0, "close_rate"],
|
||||||
axis=1)
|
text=trades.loc[trades['profitperc'] <= 0, "desc"],
|
||||||
trade_sells = go.Scatter(
|
|
||||||
x=trades["close_time"],
|
|
||||||
y=trades["close_rate"],
|
|
||||||
text=desc,
|
|
||||||
mode='markers',
|
mode='markers',
|
||||||
name='trade_sell',
|
name='Sell - Loss',
|
||||||
marker=dict(
|
marker=dict(
|
||||||
symbol='square-open',
|
symbol='square-open',
|
||||||
size=11,
|
size=11,
|
||||||
@ -142,14 +164,53 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
)
|
)
|
||||||
fig.add_trace(trade_buys, 1, 1)
|
fig.add_trace(trade_buys, 1, 1)
|
||||||
fig.add_trace(trade_sells, 1, 1)
|
fig.add_trace(trade_sells, 1, 1)
|
||||||
|
fig.add_trace(trade_sells_loss, 1, 1)
|
||||||
else:
|
else:
|
||||||
logger.warning("No trades found.")
|
logger.warning("No trades found.")
|
||||||
return fig
|
return fig
|
||||||
|
|
||||||
|
|
||||||
def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None,
|
def create_plotconfig(indicators1: List[str], indicators2: List[str],
|
||||||
|
plot_config: Dict[str, Dict]) -> Dict[str, Dict]:
|
||||||
|
"""
|
||||||
|
Combines indicators 1 and indicators 2 into plot_config if necessary
|
||||||
|
:param indicators1: List containing Main plot indicators
|
||||||
|
:param indicators2: List containing Sub plot indicators
|
||||||
|
:param plot_config: Dict of Dicts containing advanced plot configuration
|
||||||
|
:return: plot_config - eventually with indicators 1 and 2
|
||||||
|
"""
|
||||||
|
|
||||||
|
if plot_config:
|
||||||
|
if indicators1:
|
||||||
|
plot_config['main_plot'] = {ind: {} for ind in indicators1}
|
||||||
|
if indicators2:
|
||||||
|
plot_config['subplots'] = {'Other': {ind: {} for ind in indicators2}}
|
||||||
|
|
||||||
|
if not plot_config:
|
||||||
|
# If no indicators and no plot-config given, use defaults.
|
||||||
|
if not indicators1:
|
||||||
|
indicators1 = ['sma', 'ema3', 'ema5']
|
||||||
|
if not indicators2:
|
||||||
|
indicators2 = ['macd', 'macdsignal']
|
||||||
|
|
||||||
|
# Create subplot configuration if plot_config is not available.
|
||||||
|
plot_config = {
|
||||||
|
'main_plot': {ind: {} for ind in indicators1},
|
||||||
|
'subplots': {'Other': {ind: {} for ind in indicators2}},
|
||||||
|
}
|
||||||
|
if 'main_plot' not in plot_config:
|
||||||
|
plot_config['main_plot'] = {}
|
||||||
|
|
||||||
|
if 'subplots' not in plot_config:
|
||||||
|
plot_config['subplots'] = {}
|
||||||
|
return plot_config
|
||||||
|
|
||||||
|
|
||||||
|
def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *,
|
||||||
indicators1: List[str] = [],
|
indicators1: List[str] = [],
|
||||||
indicators2: List[str] = [],) -> go.Figure:
|
indicators2: List[str] = [],
|
||||||
|
plot_config: Dict[str, Dict] = {},
|
||||||
|
) -> go.Figure:
|
||||||
"""
|
"""
|
||||||
Generate the graph from the data generated by Backtesting or from DB
|
Generate the graph from the data generated by Backtesting or from DB
|
||||||
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators
|
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators
|
||||||
@ -158,21 +219,26 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
|
|||||||
:param trades: All trades created
|
:param trades: All trades created
|
||||||
:param indicators1: List containing Main plot indicators
|
:param indicators1: List containing Main plot indicators
|
||||||
:param indicators2: List containing Sub plot indicators
|
:param indicators2: List containing Sub plot indicators
|
||||||
:return: None
|
:param plot_config: Dict of Dicts containing advanced plot configuration
|
||||||
|
:return: Plotly figure
|
||||||
"""
|
"""
|
||||||
|
plot_config = create_plotconfig(indicators1, indicators2, plot_config)
|
||||||
|
|
||||||
|
rows = 2 + len(plot_config['subplots'])
|
||||||
|
row_widths = [1 for _ in plot_config['subplots']]
|
||||||
# Define the graph
|
# Define the graph
|
||||||
fig = make_subplots(
|
fig = make_subplots(
|
||||||
rows=3,
|
rows=rows,
|
||||||
cols=1,
|
cols=1,
|
||||||
shared_xaxes=True,
|
shared_xaxes=True,
|
||||||
row_width=[1, 1, 4],
|
row_width=row_widths + [1, 4],
|
||||||
vertical_spacing=0.0001,
|
vertical_spacing=0.0001,
|
||||||
)
|
)
|
||||||
fig['layout'].update(title=pair)
|
fig['layout'].update(title=pair)
|
||||||
fig['layout']['yaxis1'].update(title='Price')
|
fig['layout']['yaxis1'].update(title='Price')
|
||||||
fig['layout']['yaxis2'].update(title='Volume')
|
fig['layout']['yaxis2'].update(title='Volume')
|
||||||
fig['layout']['yaxis3'].update(title='Other')
|
for i, name in enumerate(plot_config['subplots']):
|
||||||
|
fig['layout'][f'yaxis{3 + i}'].update(title=name)
|
||||||
fig['layout']['xaxis']['rangeslider'].update(visible=False)
|
fig['layout']['xaxis']['rangeslider'].update(visible=False)
|
||||||
|
|
||||||
# Common information
|
# Common information
|
||||||
@ -242,12 +308,13 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
|
|||||||
)
|
)
|
||||||
fig.add_trace(bb_lower, 1, 1)
|
fig.add_trace(bb_lower, 1, 1)
|
||||||
fig.add_trace(bb_upper, 1, 1)
|
fig.add_trace(bb_upper, 1, 1)
|
||||||
if 'bb_upperband' in indicators1 and 'bb_lowerband' in indicators1:
|
if ('bb_upperband' in plot_config['main_plot']
|
||||||
indicators1.remove('bb_upperband')
|
and 'bb_lowerband' in plot_config['main_plot']):
|
||||||
indicators1.remove('bb_lowerband')
|
del plot_config['main_plot']['bb_upperband']
|
||||||
|
del plot_config['main_plot']['bb_lowerband']
|
||||||
|
|
||||||
# Add indicators to main plot
|
# Add indicators to main plot
|
||||||
fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data)
|
fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data)
|
||||||
|
|
||||||
fig = plot_trades(fig, trades)
|
fig = plot_trades(fig, trades)
|
||||||
|
|
||||||
@ -258,11 +325,14 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
|
|||||||
name='Volume',
|
name='Volume',
|
||||||
marker_color='DarkSlateGrey',
|
marker_color='DarkSlateGrey',
|
||||||
marker_line_color='DarkSlateGrey'
|
marker_line_color='DarkSlateGrey'
|
||||||
)
|
)
|
||||||
fig.add_trace(volume, 2, 1)
|
fig.add_trace(volume, 2, 1)
|
||||||
|
|
||||||
# Add indicators to separate row
|
# Add indicators to separate row
|
||||||
fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data)
|
for i, name in enumerate(plot_config['subplots']):
|
||||||
|
fig = add_indicators(fig=fig, row=3 + i,
|
||||||
|
indicators=plot_config['subplots'][name],
|
||||||
|
data=data)
|
||||||
|
|
||||||
return fig
|
return fig
|
||||||
|
|
||||||
@ -363,8 +433,9 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
|||||||
pair=pair,
|
pair=pair,
|
||||||
data=dataframe,
|
data=dataframe,
|
||||||
trades=trades_pair,
|
trades=trades_pair,
|
||||||
indicators1=config["indicators1"],
|
indicators1=config.get("indicators1", []),
|
||||||
indicators2=config["indicators2"],
|
indicators2=config.get("indicators2", []),
|
||||||
|
plot_config=strategy.plot_config if hasattr(strategy, 'plot_config') else {}
|
||||||
)
|
)
|
||||||
|
|
||||||
store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']),
|
store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']),
|
||||||
|
@ -88,7 +88,7 @@ class RPC:
|
|||||||
"""
|
"""
|
||||||
config = self._freqtrade.config
|
config = self._freqtrade.config
|
||||||
val = {
|
val = {
|
||||||
'dry_run': config.get('dry_run', False),
|
'dry_run': config['dry_run'],
|
||||||
'stake_currency': config['stake_currency'],
|
'stake_currency': config['stake_currency'],
|
||||||
'stake_amount': config['stake_amount'],
|
'stake_amount': config['stake_amount'],
|
||||||
'minimal_roi': config['minimal_roi'].copy(),
|
'minimal_roi': config['minimal_roi'].copy(),
|
||||||
@ -306,6 +306,8 @@ class RPC:
|
|||||||
except (TemporaryError, DependencyException):
|
except (TemporaryError, DependencyException):
|
||||||
raise RPCException('Error getting current tickers.')
|
raise RPCException('Error getting current tickers.')
|
||||||
|
|
||||||
|
self._freqtrade.wallets.update(require_update=False)
|
||||||
|
|
||||||
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
||||||
if not balance.total:
|
if not balance.total:
|
||||||
continue
|
continue
|
||||||
@ -335,7 +337,7 @@ class RPC:
|
|||||||
'stake': stake_currency,
|
'stake': stake_currency,
|
||||||
})
|
})
|
||||||
if total == 0.0:
|
if total == 0.0:
|
||||||
if self._freqtrade.config.get('dry_run', False):
|
if self._freqtrade.config['dry_run']:
|
||||||
raise RPCException('Running in Dry Run, balances are not available.')
|
raise RPCException('Running in Dry Run, balances are not available.')
|
||||||
else:
|
else:
|
||||||
raise RPCException('All balances are zero.')
|
raise RPCException('All balances are zero.')
|
||||||
@ -349,7 +351,7 @@ class RPC:
|
|||||||
'symbol': symbol,
|
'symbol': symbol,
|
||||||
'value': value,
|
'value': value,
|
||||||
'stake': stake_currency,
|
'stake': stake_currency,
|
||||||
'note': 'Simulated balances' if self._freqtrade.config.get('dry_run', False) else ''
|
'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
|
||||||
}
|
}
|
||||||
|
|
||||||
def _rpc_start(self) -> Dict[str, str]:
|
def _rpc_start(self) -> Dict[str, str]:
|
||||||
|
@ -62,7 +62,7 @@ class RPCManager:
|
|||||||
logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.")
|
logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.")
|
||||||
|
|
||||||
def startup_messages(self, config, pairlist) -> None:
|
def startup_messages(self, config, pairlist) -> None:
|
||||||
if config.get('dry_run', False):
|
if config['dry_run']:
|
||||||
self.send_msg({
|
self.send_msg({
|
||||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
'type': RPCMessageType.WARNING_NOTIFICATION,
|
||||||
'status': 'Dry run is enabled. All trades are simulated.'
|
'status': 'Dry run is enabled. All trades are simulated.'
|
||||||
|
@ -112,6 +112,9 @@ class IStrategy(ABC):
|
|||||||
dp: Optional[DataProvider] = None
|
dp: Optional[DataProvider] = None
|
||||||
wallets: Optional[Wallets] = None
|
wallets: Optional[Wallets] = None
|
||||||
|
|
||||||
|
# Definition of plot_config. See plotting documentation for more details.
|
||||||
|
plot_config: Dict = {}
|
||||||
|
|
||||||
def __init__(self, config: dict) -> None:
|
def __init__(self, config: dict) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
# Dict to determine if analysis is necessary
|
# Dict to determine if analysis is necessary
|
||||||
@ -386,9 +389,11 @@ class IStrategy(ABC):
|
|||||||
trade.adjust_stop_loss(high or current_rate, stop_loss_value)
|
trade.adjust_stop_loss(high or current_rate, stop_loss_value)
|
||||||
|
|
||||||
# evaluate if the stoploss was hit if stoploss is not on exchange
|
# evaluate if the stoploss was hit if stoploss is not on exchange
|
||||||
|
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
|
||||||
|
# regular stoploss handling.
|
||||||
if ((self.stoploss is not None) and
|
if ((self.stoploss is not None) and
|
||||||
(trade.stop_loss >= current_rate) and
|
(trade.stop_loss >= current_rate) and
|
||||||
(not self.order_types.get('stoploss_on_exchange'))):
|
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
|
||||||
|
|
||||||
sell_type = SellType.STOP_LOSS
|
sell_type = SellType.STOP_LOSS
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ class {{ strategy }}(IStrategy):
|
|||||||
'buy': 'gtc',
|
'buy': 'gtc',
|
||||||
'sell': 'gtc'
|
'sell': 'gtc'
|
||||||
}
|
}
|
||||||
|
{{ plot_config | indent(4) }}
|
||||||
def informative_pairs(self):
|
def informative_pairs(self):
|
||||||
"""
|
"""
|
||||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||||
|
@ -80,6 +80,22 @@ class SampleStrategy(IStrategy):
|
|||||||
'sell': 'gtc'
|
'sell': 'gtc'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plot_config = {
|
||||||
|
'main_plot': {
|
||||||
|
'tema': {},
|
||||||
|
'sar': {'color': 'white'},
|
||||||
|
},
|
||||||
|
'subplots': {
|
||||||
|
"MACD": {
|
||||||
|
'macd': {'color': 'blue'},
|
||||||
|
'macdsignal': {'color': 'orange'},
|
||||||
|
},
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def informative_pairs(self):
|
def informative_pairs(self):
|
||||||
"""
|
"""
|
||||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||||
|
18
freqtrade/templates/subtemplates/plot_config_full.j2
Normal file
18
freqtrade/templates/subtemplates/plot_config_full.j2
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
plot_config = {
|
||||||
|
# Main plot indicators (Moving averages, ...)
|
||||||
|
'main_plot': {
|
||||||
|
'tema': {},
|
||||||
|
'sar': {'color': 'white'},
|
||||||
|
},
|
||||||
|
'subplots': {
|
||||||
|
# Subplots - each dict defines one additional plot
|
||||||
|
"MACD": {
|
||||||
|
'macd': {'color': 'blue'},
|
||||||
|
'macdsignal': {'color': 'orange'},
|
||||||
|
},
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -104,12 +104,14 @@ def deploy_new_strategy(strategy_name, strategy_path: Path, subtemplate: str):
|
|||||||
indicators = render_template(templatefile=f"subtemplates/indicators_{subtemplate}.j2",)
|
indicators = render_template(templatefile=f"subtemplates/indicators_{subtemplate}.j2",)
|
||||||
buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",)
|
buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",)
|
||||||
sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",)
|
sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",)
|
||||||
|
plot_config = render_template(templatefile=f"subtemplates/plot_config_{subtemplate}.j2",)
|
||||||
|
|
||||||
strategy_text = render_template(templatefile='base_strategy.py.j2',
|
strategy_text = render_template(templatefile='base_strategy.py.j2',
|
||||||
arguments={"strategy": strategy_name,
|
arguments={"strategy": strategy_name,
|
||||||
"indicators": indicators,
|
"indicators": indicators,
|
||||||
"buy_trend": buy_trend,
|
"buy_trend": buy_trend,
|
||||||
"sell_trend": sell_trend,
|
"sell_trend": sell_trend,
|
||||||
|
"plot_config": plot_config,
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info(f"Writing strategy to `{strategy_path}`.")
|
logger.info(f"Writing strategy to `{strategy_path}`.")
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
""" Wallet """
|
""" Wallet """
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, NamedTuple, Any
|
from typing import Any, Dict, NamedTuple
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
@ -24,7 +27,7 @@ class Wallets:
|
|||||||
self._exchange = exchange
|
self._exchange = exchange
|
||||||
self._wallets: Dict[str, Wallet] = {}
|
self._wallets: Dict[str, Wallet] = {}
|
||||||
self.start_cap = config['dry_run_wallet']
|
self.start_cap = config['dry_run_wallet']
|
||||||
|
self._last_wallet_refresh = 0
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def get_free(self, currency) -> float:
|
def get_free(self, currency) -> float:
|
||||||
@ -95,12 +98,21 @@ class Wallets:
|
|||||||
balances[currency].get('total', None)
|
balances[currency].get('total', None)
|
||||||
)
|
)
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self, require_update: bool = True) -> None:
|
||||||
if self._config['dry_run']:
|
"""
|
||||||
self._update_dry()
|
Updates wallets from the configured version.
|
||||||
else:
|
By default, updates from the exchange.
|
||||||
self._update_live()
|
Update-skipping should only be used for user-invoked /balance calls, since
|
||||||
logger.info('Wallets synced.')
|
for trading operations, the latest balance is needed.
|
||||||
|
:param require_update: Allow skipping an update if balances were recently refreshed
|
||||||
|
"""
|
||||||
|
if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().timestamp)):
|
||||||
|
if self._config['dry_run']:
|
||||||
|
self._update_dry()
|
||||||
|
else:
|
||||||
|
self._update_live()
|
||||||
|
logger.info('Wallets synced.')
|
||||||
|
self._last_wallet_refresh = arrow.utcnow().timestamp
|
||||||
|
|
||||||
def get_all_balances(self) -> Dict[str, Any]:
|
def get_all_balances(self) -> Dict[str, Any]:
|
||||||
return self._wallets
|
return self._wallets
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# requirements without requirements installable via conda
|
# requirements without requirements installable via conda
|
||||||
# mainly used for Raspberry pi installs
|
# mainly used for Raspberry pi installs
|
||||||
ccxt==1.21.23
|
ccxt==1.21.76
|
||||||
SQLAlchemy==1.3.12
|
SQLAlchemy==1.3.12
|
||||||
python-telegram-bot==12.2.0
|
python-telegram-bot==12.3.0
|
||||||
arrow==0.15.4
|
arrow==0.15.5
|
||||||
cachetools==4.0.0
|
cachetools==4.0.0
|
||||||
requests==2.22.0
|
requests==2.22.0
|
||||||
urllib3==1.25.7
|
urllib3==1.25.7
|
||||||
|
@ -3,15 +3,15 @@
|
|||||||
-r requirements-plot.txt
|
-r requirements-plot.txt
|
||||||
-r requirements-hyperopt.txt
|
-r requirements-hyperopt.txt
|
||||||
|
|
||||||
coveralls==1.9.2
|
coveralls==1.10.0
|
||||||
flake8==3.7.9
|
flake8==3.7.9
|
||||||
flake8-type-annotations==0.1.0
|
flake8-type-annotations==0.1.0
|
||||||
flake8-tidy-imports==3.1.0
|
flake8-tidy-imports==4.0.0
|
||||||
mypy==0.761
|
mypy==0.761
|
||||||
pytest==5.3.2
|
pytest==5.3.3
|
||||||
pytest-asyncio==0.10.0
|
pytest-asyncio==0.10.0
|
||||||
pytest-cov==2.8.1
|
pytest-cov==2.8.1
|
||||||
pytest-mock==1.13.0
|
pytest-mock==2.0.0
|
||||||
pytest-random-order==1.0.4
|
pytest-random-order==1.0.4
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scipy==1.4.1
|
scipy==1.4.1
|
||||||
scikit-learn==0.22
|
scikit-learn==0.22.1
|
||||||
scikit-optimize==0.5.2
|
scikit-optimize==0.5.2
|
||||||
filelock==3.0.12
|
filelock==3.0.12
|
||||||
joblib==0.14.1
|
joblib==0.14.1
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Load common requirements
|
# Load common requirements
|
||||||
-r requirements-common.txt
|
-r requirements-common.txt
|
||||||
|
|
||||||
numpy==1.18.0
|
numpy==1.18.1
|
||||||
pandas==0.25.3
|
pandas==0.25.3
|
||||||
|
@ -60,8 +60,10 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
|
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
|
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
|
||||||
if mock_markets:
|
if mock_markets:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.markets',
|
mocker.patch('freqtrade.exchange.Exchange.markets',
|
||||||
PropertyMock(return_value=get_markets()))
|
PropertyMock(return_value=get_markets()))
|
||||||
@ -1317,12 +1319,12 @@ def buy_order_fee():
|
|||||||
def edge_conf(default_conf):
|
def edge_conf(default_conf):
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['max_open_trades'] = -1
|
conf['max_open_trades'] = -1
|
||||||
|
conf['tradable_balance_ratio'] = 0.5
|
||||||
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||||
conf['edge'] = {
|
conf['edge'] = {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"process_throttle_secs": 1800,
|
"process_throttle_secs": 1800,
|
||||||
"calculate_since_number_of_days": 14,
|
"calculate_since_number_of_days": 14,
|
||||||
"capital_available_percentage": 0.5,
|
|
||||||
"allowed_risk": 0.01,
|
"allowed_risk": 0.01,
|
||||||
"stoploss_range_min": -0.01,
|
"stoploss_range_min": -0.01,
|
||||||
"stoploss_range_max": -0.1,
|
"stoploss_range_max": -0.1,
|
||||||
|
@ -22,8 +22,8 @@ def test_stoploss_limit_order(default_conf, mocker):
|
|||||||
})
|
})
|
||||||
|
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
|
||||||
@ -71,8 +71,8 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
|||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_type = 'stop_loss_limit'
|
order_type = 'stop_loss_limit'
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
|
||||||
|
@ -73,6 +73,7 @@ def test_init(default_conf, mocker, caplog):
|
|||||||
|
|
||||||
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
conf = copy.deepcopy(default_conf)
|
conf = copy.deepcopy(default_conf)
|
||||||
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True}
|
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True}
|
||||||
@ -121,9 +122,10 @@ def test_init_exception(default_conf, mocker):
|
|||||||
|
|
||||||
def test_exchange_resolver(default_conf, mocker, caplog):
|
def test_exchange_resolver(default_conf, mocker, caplog):
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock()))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock()))
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
exchange = ExchangeResolver.load_exchange('Bittrex', default_conf)
|
exchange = ExchangeResolver.load_exchange('Bittrex', default_conf)
|
||||||
assert isinstance(exchange, Exchange)
|
assert isinstance(exchange, Exchange)
|
||||||
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
||||||
@ -173,35 +175,78 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
|
|||||||
ex.validate_order_time_in_force(tif2)
|
ex.validate_order_time_in_force(tif2)
|
||||||
|
|
||||||
|
|
||||||
def test_symbol_amount_prec(default_conf, mocker):
|
@pytest.mark.parametrize("amount,precision_mode,precision,expected", [
|
||||||
|
(2.34559, 2, 4, 2.3455),
|
||||||
|
(2.34559, 2, 5, 2.34559),
|
||||||
|
(2.34559, 2, 3, 2.345),
|
||||||
|
(2.9999, 2, 3, 2.999),
|
||||||
|
(2.9909, 2, 3, 2.990),
|
||||||
|
# Tests for Tick-size
|
||||||
|
(2.34559, 4, 0.0001, 2.3455),
|
||||||
|
(2.34559, 4, 0.00001, 2.34559),
|
||||||
|
(2.34559, 4, 0.001, 2.345),
|
||||||
|
(2.9999, 4, 0.001, 2.999),
|
||||||
|
(2.9909, 4, 0.001, 2.990),
|
||||||
|
(2.9909, 4, 0.005, 2.990),
|
||||||
|
(2.9999, 4, 0.005, 2.995),
|
||||||
|
])
|
||||||
|
def test_amount_to_precision(default_conf, mocker, amount, precision_mode, precision, expected):
|
||||||
'''
|
'''
|
||||||
Test rounds down to 4 Decimal places
|
Test rounds down
|
||||||
'''
|
'''
|
||||||
|
|
||||||
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}})
|
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': precision}}})
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||||
|
# digits counting mode
|
||||||
|
# DECIMAL_PLACES = 2
|
||||||
|
# SIGNIFICANT_DIGITS = 3
|
||||||
|
# TICK_SIZE = 4
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.precisionMode',
|
||||||
|
PropertyMock(return_value=precision_mode))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
|
||||||
|
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
assert exchange.amount_to_precision(pair, amount) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
|
||||||
|
(2.34559, 2, 4, 2.3456),
|
||||||
|
(2.34559, 2, 5, 2.34559),
|
||||||
|
(2.34559, 2, 3, 2.346),
|
||||||
|
(2.9999, 2, 3, 3.000),
|
||||||
|
(2.9909, 2, 3, 2.991),
|
||||||
|
# Tests for Tick_size
|
||||||
|
(2.34559, 4, 0.0001, 2.3456),
|
||||||
|
(2.34559, 4, 0.00001, 2.34559),
|
||||||
|
(2.34559, 4, 0.001, 2.346),
|
||||||
|
(2.9999, 4, 0.001, 3.000),
|
||||||
|
(2.9909, 4, 0.001, 2.991),
|
||||||
|
(2.9909, 4, 0.005, 2.995),
|
||||||
|
(2.9973, 4, 0.005, 3.0),
|
||||||
|
(2.9977, 4, 0.005, 3.0),
|
||||||
|
(234.43, 4, 0.5, 234.5),
|
||||||
|
(234.53, 4, 0.5, 235.0),
|
||||||
|
(0.891534, 4, 0.0001, 0.8916),
|
||||||
|
|
||||||
|
])
|
||||||
|
def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected):
|
||||||
|
'''
|
||||||
|
Test price to precision
|
||||||
|
'''
|
||||||
|
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': precision}}})
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||||
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
|
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
|
||||||
|
# digits counting mode
|
||||||
|
# DECIMAL_PLACES = 2
|
||||||
|
# SIGNIFICANT_DIGITS = 3
|
||||||
|
# TICK_SIZE = 4
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.precisionMode',
|
||||||
|
PropertyMock(return_value=precision_mode))
|
||||||
|
|
||||||
amount = 2.34559
|
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
amount = exchange.symbol_amount_prec(pair, amount)
|
assert pytest.approx(exchange.price_to_precision(pair, price)) == expected
|
||||||
assert amount == 2.3455
|
|
||||||
|
|
||||||
|
|
||||||
def test_symbol_price_prec(default_conf, mocker):
|
|
||||||
'''
|
|
||||||
Test rounds up to 4 decimal places
|
|
||||||
'''
|
|
||||||
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}})
|
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
|
|
||||||
|
|
||||||
price = 2.34559
|
|
||||||
pair = 'ETH/BTC'
|
|
||||||
price = exchange.symbol_price_prec(pair, price)
|
|
||||||
assert price == 2.3456
|
|
||||||
|
|
||||||
|
|
||||||
def test_set_sandbox(default_conf, mocker):
|
def test_set_sandbox(default_conf, mocker):
|
||||||
@ -257,9 +302,10 @@ def test__load_markets(default_conf, mocker, caplog):
|
|||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError"))
|
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError"))
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
assert log_has('Unable to initialize markets. Reason: SomeError', caplog)
|
assert log_has('Unable to initialize markets. Reason: SomeError', caplog)
|
||||||
|
|
||||||
@ -315,6 +361,44 @@ def test__reload_markets_exception(default_conf, mocker, caplog):
|
|||||||
assert log_has_re(r"Could not reload markets.*", caplog)
|
assert log_has_re(r"Could not reload markets.*", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT'])
|
||||||
|
def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog):
|
||||||
|
default_conf['stake_currency'] = stake_currency
|
||||||
|
api_mock = MagicMock()
|
||||||
|
type(api_mock).markets = PropertyMock(return_value={
|
||||||
|
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
|
||||||
|
'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'},
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_stake_currency_error(default_conf, mocker, caplog):
|
||||||
|
default_conf['stake_currency'] = 'XRP'
|
||||||
|
api_mock = MagicMock()
|
||||||
|
type(api_mock).markets = PropertyMock(return_value={
|
||||||
|
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
|
||||||
|
'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'},
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'XRP is not available as stake on .*'
|
||||||
|
'Available currencies are: BTC, ETH, USDT'):
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_quote_currencies(default_conf, mocker):
|
||||||
|
ex = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
|
assert set(ex.get_quote_currencies()) == set(['USD', 'BTC', 'USDT'])
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly
|
def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
type(api_mock).markets = PropertyMock(return_value={
|
type(api_mock).markets = PropertyMock(return_value={
|
||||||
@ -324,8 +408,9 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d
|
|||||||
type(api_mock).id = id_mock
|
type(api_mock).id = id_mock
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
@ -335,8 +420,9 @@ def test_validate_pairs_not_available(default_conf, mocker):
|
|||||||
'XRP/BTC': {'inactive': True}
|
'XRP/BTC': {'inactive': True}
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'not available'):
|
with pytest.raises(OperationalException, match=r'not available'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
@ -349,8 +435,9 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
|
|||||||
|
|
||||||
type(api_mock).markets = PropertyMock(return_value={})
|
type(api_mock).markets = PropertyMock(return_value={})
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'):
|
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
@ -368,8 +455,9 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
|
|||||||
'NEO/BTC': {'info': 'TestString'}, # info can also be a string ...
|
'NEO/BTC': {'info': 'TestString'}, # info can also be a string ...
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
|
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange."
|
assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange."
|
||||||
@ -377,8 +465,11 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
|
|||||||
f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
|
f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_timeframes(default_conf, mocker):
|
@pytest.mark.parametrize("timeframe", [
|
||||||
default_conf["ticker_interval"] = "5m"
|
('5m'), ("1m"), ("15m"), ("1h")
|
||||||
|
])
|
||||||
|
def test_validate_timeframes(default_conf, mocker, timeframe):
|
||||||
|
default_conf["ticker_interval"] = timeframe
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
id_mock = PropertyMock(return_value='test_exchange')
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
type(api_mock).id = id_mock
|
type(api_mock).id = id_mock
|
||||||
@ -390,7 +481,8 @@ def test_validate_timeframes(default_conf, mocker):
|
|||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
@ -399,7 +491,8 @@ def test_validate_timeframes_failed(default_conf, mocker):
|
|||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
id_mock = PropertyMock(return_value='test_exchange')
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
type(api_mock).id = id_mock
|
type(api_mock).id = id_mock
|
||||||
timeframes = PropertyMock(return_value={'1m': '1m',
|
timeframes = PropertyMock(return_value={'15s': '15s',
|
||||||
|
'1m': '1m',
|
||||||
'5m': '5m',
|
'5m': '5m',
|
||||||
'15m': '15m',
|
'15m': '15m',
|
||||||
'1h': '1h'})
|
'1h': '1h'})
|
||||||
@ -411,6 +504,11 @@ def test_validate_timeframes_failed(default_conf, mocker):
|
|||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r"Invalid ticker interval '3m'. This exchange supports.*"):
|
match=r"Invalid ticker interval '3m'. This exchange supports.*"):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
default_conf["ticker_interval"] = "15s"
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r"Timeframes < 1m are currently not supported by Freqtrade."):
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
|
def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
|
||||||
@ -424,7 +522,8 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
|
|||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r'The ccxt library does not provide the list of timeframes '
|
match=r'The ccxt library does not provide the list of timeframes '
|
||||||
r'for the exchange ".*" and this exchange '
|
r'for the exchange ".*" and this exchange '
|
||||||
@ -445,6 +544,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets',
|
mocker.patch('freqtrade.exchange.Exchange._load_markets',
|
||||||
MagicMock(return_value={'timeframes': None}))
|
MagicMock(return_value={'timeframes': None}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r'The ccxt library does not provide the list of timeframes '
|
match=r'The ccxt library does not provide the list of timeframes '
|
||||||
r'for the exchange ".*" and this exchange '
|
r'for the exchange ".*" and this exchange '
|
||||||
@ -465,7 +565,8 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
|
|||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
@ -475,8 +576,9 @@ def test_validate_order_types(default_conf, mocker):
|
|||||||
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True})
|
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True})
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
|
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
|
||||||
default_conf['order_types'] = {
|
default_conf['order_types'] = {
|
||||||
'buy': 'limit',
|
'buy': 'limit',
|
||||||
@ -517,8 +619,9 @@ def test_validate_order_types_not_in_config(default_conf, mocker):
|
|||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
|
|
||||||
conf = copy.deepcopy(default_conf)
|
conf = copy.deepcopy(default_conf)
|
||||||
Exchange(conf)
|
Exchange(conf)
|
||||||
@ -529,9 +632,10 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance'))
|
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance'))
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
|
|
||||||
default_conf['startup_candle_count'] = 20
|
default_conf['startup_candle_count'] = 20
|
||||||
ex = Exchange(default_conf)
|
ex = Exchange(default_conf)
|
||||||
@ -596,8 +700,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
|
||||||
order = exchange.create_order(
|
order = exchange.create_order(
|
||||||
@ -637,8 +741,8 @@ def test_buy_prod(default_conf, mocker, exchange_name):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
|
||||||
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
@ -711,8 +815,8 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
|
||||||
order_type = 'limit'
|
order_type = 'limit'
|
||||||
@ -773,8 +877,8 @@ def test_sell_prod(default_conf, mocker, exchange_name):
|
|||||||
})
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
|
||||||
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||||
@ -837,8 +941,8 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
|
|||||||
})
|
})
|
||||||
api_mock.options = {}
|
api_mock.options = {}
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
|
||||||
order_type = 'limit'
|
order_type = 'limit'
|
||||||
@ -1665,7 +1769,9 @@ def test_merge_ft_has_dict(default_conf, mocker):
|
|||||||
_init_ccxt=MagicMock(return_value=MagicMock()),
|
_init_ccxt=MagicMock(return_value=MagicMock()),
|
||||||
_load_async_markets=MagicMock(),
|
_load_async_markets=MagicMock(),
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
validate_timeframes=MagicMock())
|
validate_timeframes=MagicMock(),
|
||||||
|
validate_stakecurrency=MagicMock()
|
||||||
|
)
|
||||||
ex = Exchange(default_conf)
|
ex = Exchange(default_conf)
|
||||||
assert ex._ft_has == Exchange._ft_has_default
|
assert ex._ft_has == Exchange._ft_has_default
|
||||||
|
|
||||||
|
@ -21,8 +21,8 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
|
|||||||
})
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||||
|
|
||||||
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
@ -53,8 +53,8 @@ def test_sell_kraken_trading_agreement(default_conf, mocker):
|
|||||||
})
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||||
|
|
||||||
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||||
|
@ -325,105 +325,6 @@ def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
|
|||||||
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
|
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table(default_conf, mocker):
|
|
||||||
patch_exchange(mocker)
|
|
||||||
default_conf['max_open_trades'] = 2
|
|
||||||
backtesting = Backtesting(default_conf)
|
|
||||||
|
|
||||||
results = pd.DataFrame(
|
|
||||||
{
|
|
||||||
'pair': ['ETH/BTC', 'ETH/BTC'],
|
|
||||||
'profit_percent': [0.1, 0.2],
|
|
||||||
'profit_abs': [0.2, 0.4],
|
|
||||||
'trade_duration': [10, 30],
|
|
||||||
'profit': [2, 0],
|
|
||||||
'loss': [0, 0]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
result_str = (
|
|
||||||
'| pair | buy count | avg profit % | cum profit % | '
|
|
||||||
'tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
|
||||||
'|:--------|------------:|---------------:|---------------:|'
|
|
||||||
'-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
|
||||||
'| ETH/BTC | 2 | 15.00 | 30.00 | '
|
|
||||||
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n'
|
|
||||||
'| TOTAL | 2 | 15.00 | 30.00 | '
|
|
||||||
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |'
|
|
||||||
)
|
|
||||||
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table_sell_reason(default_conf, mocker):
|
|
||||||
patch_exchange(mocker)
|
|
||||||
backtesting = Backtesting(default_conf)
|
|
||||||
|
|
||||||
results = pd.DataFrame(
|
|
||||||
{
|
|
||||||
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
|
||||||
'profit_percent': [0.1, 0.2, -0.3],
|
|
||||||
'profit_abs': [0.2, 0.4, -0.5],
|
|
||||||
'trade_duration': [10, 30, 10],
|
|
||||||
'profit': [2, 0, 0],
|
|
||||||
'loss': [0, 0, 1],
|
|
||||||
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
result_str = (
|
|
||||||
'| Sell Reason | Count | Profit | Loss |\n'
|
|
||||||
'|:--------------|--------:|---------:|-------:|\n'
|
|
||||||
'| roi | 2 | 2 | 0 |\n'
|
|
||||||
'| stop_loss | 1 | 0 | 1 |'
|
|
||||||
)
|
|
||||||
assert backtesting._generate_text_table_sell_reason(
|
|
||||||
data={'ETH/BTC': {}}, results=results) == result_str
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table_strategyn(default_conf, mocker):
|
|
||||||
"""
|
|
||||||
Test Backtesting.generate_text_table_sell_reason() method
|
|
||||||
"""
|
|
||||||
patch_exchange(mocker)
|
|
||||||
default_conf['max_open_trades'] = 2
|
|
||||||
backtesting = Backtesting(default_conf)
|
|
||||||
results = {}
|
|
||||||
results['ETH/BTC'] = pd.DataFrame(
|
|
||||||
{
|
|
||||||
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
|
||||||
'profit_percent': [0.1, 0.2, 0.3],
|
|
||||||
'profit_abs': [0.2, 0.4, 0.5],
|
|
||||||
'trade_duration': [10, 30, 10],
|
|
||||||
'profit': [2, 0, 0],
|
|
||||||
'loss': [0, 0, 1],
|
|
||||||
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
results['LTC/BTC'] = pd.DataFrame(
|
|
||||||
{
|
|
||||||
'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'],
|
|
||||||
'profit_percent': [0.4, 0.2, 0.3],
|
|
||||||
'profit_abs': [0.4, 0.4, 0.5],
|
|
||||||
'trade_duration': [15, 30, 15],
|
|
||||||
'profit': [4, 1, 0],
|
|
||||||
'loss': [0, 0, 1],
|
|
||||||
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
result_str = (
|
|
||||||
'| Strategy | buy count | avg profit % | cum profit % '
|
|
||||||
'| tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
|
||||||
'|:-----------|------------:|---------------:|---------------:'
|
|
||||||
'|-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
|
||||||
'| ETH/BTC | 3 | 20.00 | 60.00 '
|
|
||||||
'| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n'
|
|
||||||
'| LTC/BTC | 3 | 30.00 | 90.00 '
|
|
||||||
'| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |'
|
|
||||||
)
|
|
||||||
assert backtesting._generate_text_table_strategy(all_results=results) == result_str
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
||||||
def get_timerange(input1):
|
def get_timerange(input1):
|
||||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||||
@ -431,11 +332,8 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
|||||||
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
|
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
||||||
'freqtrade.optimize.backtesting.Backtesting',
|
mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1))
|
||||||
backtest=MagicMock(),
|
|
||||||
_generate_text_table=MagicMock(return_value='1'),
|
|
||||||
)
|
|
||||||
|
|
||||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
default_conf['ticker_interval'] = '1m'
|
default_conf['ticker_interval'] = '1m'
|
||||||
@ -465,11 +363,8 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
|
|||||||
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
|
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
||||||
'freqtrade.optimize.backtesting.Backtesting',
|
mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1))
|
||||||
backtest=MagicMock(),
|
|
||||||
_generate_text_table=MagicMock(return_value='1'),
|
|
||||||
)
|
|
||||||
|
|
||||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
default_conf['ticker_interval'] = "1m"
|
default_conf['ticker_interval'] = "1m"
|
||||||
@ -774,7 +669,8 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
|
|||||||
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
|
mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock())
|
||||||
|
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
@ -815,10 +711,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
|||||||
backtestmock = MagicMock()
|
backtestmock = MagicMock()
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||||
gen_table_mock = MagicMock()
|
gen_table_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', gen_table_mock)
|
mocker.patch('freqtrade.optimize.backtesting.generate_text_table', gen_table_mock)
|
||||||
gen_strattable_mock = MagicMock()
|
gen_strattable_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy',
|
mocker.patch('freqtrade.optimize.backtesting.generate_text_table_strategy', gen_strattable_mock)
|
||||||
gen_strattable_mock)
|
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.edge import PairInfo
|
|
||||||
from freqtrade.optimize import setup_configuration, start_edge
|
from freqtrade.optimize import setup_configuration, start_edge
|
||||||
from freqtrade.optimize.edge_cli import EdgeCli
|
from freqtrade.optimize.edge_cli import EdgeCli
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
@ -106,16 +105,3 @@ def test_edge_init_fee(mocker, edge_conf) -> None:
|
|||||||
edge_cli = EdgeCli(edge_conf)
|
edge_cli = EdgeCli(edge_conf)
|
||||||
assert edge_cli.edge.fee == 0.1234
|
assert edge_cli.edge.fee == 0.1234
|
||||||
assert fee_mock.call_count == 0
|
assert fee_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_generate_edge_table(edge_conf, mocker):
|
|
||||||
patch_exchange(mocker)
|
|
||||||
edge_cli = EdgeCli(edge_conf)
|
|
||||||
|
|
||||||
results = {}
|
|
||||||
results['ETH/BTC'] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60)
|
|
||||||
|
|
||||||
assert edge_cli._generate_edge_table(results).count(':|') == 7
|
|
||||||
assert edge_cli._generate_edge_table(results).count('| ETH/BTC |') == 1
|
|
||||||
assert edge_cli._generate_edge_table(results).count(
|
|
||||||
'| risk reward ratio | required risk reward | expectancy |') == 1
|
|
||||||
|
108
tests/optimize/test_optimize_reports.py
Normal file
108
tests/optimize/test_optimize_reports.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from freqtrade.edge import PairInfo
|
||||||
|
from freqtrade.optimize.optimize_reports import (
|
||||||
|
generate_edge_table, generate_text_table, generate_text_table_sell_reason,
|
||||||
|
generate_text_table_strategy)
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_text_table(default_conf, mocker):
|
||||||
|
|
||||||
|
results = pd.DataFrame(
|
||||||
|
{
|
||||||
|
'pair': ['ETH/BTC', 'ETH/BTC'],
|
||||||
|
'profit_percent': [0.1, 0.2],
|
||||||
|
'profit_abs': [0.2, 0.4],
|
||||||
|
'trade_duration': [10, 30],
|
||||||
|
'profit': [2, 0],
|
||||||
|
'loss': [0, 0]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result_str = (
|
||||||
|
'| pair | buy count | avg profit % | cum profit % | '
|
||||||
|
'tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
||||||
|
'|:--------|------------:|---------------:|---------------:|'
|
||||||
|
'-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
||||||
|
'| ETH/BTC | 2 | 15.00 | 30.00 | '
|
||||||
|
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n'
|
||||||
|
'| TOTAL | 2 | 15.00 | 30.00 | '
|
||||||
|
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |'
|
||||||
|
)
|
||||||
|
assert generate_text_table(data={'ETH/BTC': {}},
|
||||||
|
stake_currency='BTC', max_open_trades=2,
|
||||||
|
results=results) == result_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_text_table_sell_reason(default_conf, mocker):
|
||||||
|
|
||||||
|
results = pd.DataFrame(
|
||||||
|
{
|
||||||
|
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
||||||
|
'profit_percent': [0.1, 0.2, -0.1],
|
||||||
|
'profit_abs': [0.2, 0.4, -0.2],
|
||||||
|
'trade_duration': [10, 30, 10],
|
||||||
|
'profit': [2, 0, 0],
|
||||||
|
'loss': [0, 0, 1],
|
||||||
|
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result_str = (
|
||||||
|
'| Sell Reason | Count | Profit | Loss | Profit % |\n'
|
||||||
|
'|:--------------|--------:|---------:|-------:|-----------:|\n'
|
||||||
|
'| roi | 2 | 2 | 0 | 15 |\n'
|
||||||
|
'| stop_loss | 1 | 0 | 1 | -10 |'
|
||||||
|
)
|
||||||
|
assert generate_text_table_sell_reason(
|
||||||
|
data={'ETH/BTC': {}}, results=results) == result_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_text_table_strategy(default_conf, mocker):
|
||||||
|
results = {}
|
||||||
|
results['ETH/BTC'] = pd.DataFrame(
|
||||||
|
{
|
||||||
|
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
||||||
|
'profit_percent': [0.1, 0.2, 0.3],
|
||||||
|
'profit_abs': [0.2, 0.4, 0.5],
|
||||||
|
'trade_duration': [10, 30, 10],
|
||||||
|
'profit': [2, 0, 0],
|
||||||
|
'loss': [0, 0, 1],
|
||||||
|
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
results['LTC/BTC'] = pd.DataFrame(
|
||||||
|
{
|
||||||
|
'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'],
|
||||||
|
'profit_percent': [0.4, 0.2, 0.3],
|
||||||
|
'profit_abs': [0.4, 0.4, 0.5],
|
||||||
|
'trade_duration': [15, 30, 15],
|
||||||
|
'profit': [4, 1, 0],
|
||||||
|
'loss': [0, 0, 1],
|
||||||
|
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result_str = (
|
||||||
|
'| Strategy | buy count | avg profit % | cum profit % '
|
||||||
|
'| tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
||||||
|
'|:-----------|------------:|---------------:|---------------:'
|
||||||
|
'|-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
||||||
|
'| ETH/BTC | 3 | 20.00 | 60.00 '
|
||||||
|
'| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n'
|
||||||
|
'| LTC/BTC | 3 | 30.00 | 90.00 '
|
||||||
|
'| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |'
|
||||||
|
)
|
||||||
|
assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_edge_table(edge_conf, mocker):
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
results['ETH/BTC'] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60)
|
||||||
|
|
||||||
|
assert generate_edge_table(results).count(':|') == 7
|
||||||
|
assert generate_edge_table(results).count('| ETH/BTC |') == 1
|
||||||
|
assert generate_edge_table(results).count(
|
||||||
|
'| risk reward ratio | required risk reward | expectancy |') == 1
|
@ -513,6 +513,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
|||||||
),
|
),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=1000)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
patch_get_signal(freqtradebot, (True, False))
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
@ -323,7 +323,7 @@ def test_load_dry_run(default_conf, mocker, config_value, expected, arglist) ->
|
|||||||
configuration = Configuration(Arguments(arglist).get_parsed_arg())
|
configuration = Configuration(Arguments(arglist).get_parsed_arg())
|
||||||
validated_conf = configuration.load_config()
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
assert validated_conf.get('dry_run') is expected
|
assert validated_conf['dry_run'] is expected
|
||||||
|
|
||||||
|
|
||||||
def test_load_custom_strategy(default_conf, mocker) -> None:
|
def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||||
@ -723,6 +723,14 @@ def test_validate_default_conf(default_conf) -> None:
|
|||||||
validate_config_schema(default_conf)
|
validate_config_schema(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_max_open_trades(default_conf):
|
||||||
|
default_conf['max_open_trades'] = float('inf')
|
||||||
|
default_conf['stake_amount'] = 'unlimited'
|
||||||
|
with pytest.raises(OperationalException, match='`max_open_trades` and `stake_amount` '
|
||||||
|
'cannot both be unlimited.'):
|
||||||
|
validate_config_consistency(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_tsl(default_conf):
|
def test_validate_tsl(default_conf):
|
||||||
default_conf['stoploss'] = 0.0
|
default_conf['stoploss'] = 0.0
|
||||||
with pytest.raises(OperationalException, match='The config stoploss needs to be different '
|
with pytest.raises(OperationalException, match='The config stoploss needs to be different '
|
||||||
@ -1029,6 +1037,17 @@ def test_process_deprecated_setting_pairlists(mocker, default_conf, caplog):
|
|||||||
assert log_has_re(r'DEPRECATED.*in pairlist is deprecated and must be moved*', caplog)
|
assert log_has_re(r'DEPRECATED.*in pairlist is deprecated and must be moved*', caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_deprecated_setting_edge(mocker, edge_conf, caplog):
|
||||||
|
patched_configuration_load_config_file(mocker, edge_conf)
|
||||||
|
edge_conf.update({'edge': {
|
||||||
|
'enabled': True,
|
||||||
|
'capital_available_percentage': 0.5,
|
||||||
|
}})
|
||||||
|
|
||||||
|
process_temporary_deprecated_settings(edge_conf)
|
||||||
|
assert log_has_re(r"DEPRECATED.*Using 'edge.capital_available_percentage'*", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_check_conflicting_settings(mocker, default_conf, caplog):
|
def test_check_conflicting_settings(mocker, default_conf, caplog):
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
|
||||||
|
@ -140,21 +140,65 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None:
|
|||||||
assert result == default_conf['stake_amount']
|
assert result == default_conf['stake_amount']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("amend_last,wallet,max_open,lsamr,expected", [
|
||||||
|
(False, 0.002, 2, 0.5, [0.001, None]),
|
||||||
|
(True, 0.002, 2, 0.5, [0.001, 0.00098]),
|
||||||
|
(False, 0.003, 3, 0.5, [0.001, 0.001, None]),
|
||||||
|
(True, 0.003, 3, 0.5, [0.001, 0.001, 0.00097]),
|
||||||
|
(False, 0.0022, 3, 0.5, [0.001, 0.001, None]),
|
||||||
|
(True, 0.0022, 3, 0.5, [0.001, 0.001, 0.0]),
|
||||||
|
(True, 0.0027, 3, 0.5, [0.001, 0.001, 0.000673]),
|
||||||
|
(True, 0.0022, 3, 1, [0.001, 0.001, 0.0]),
|
||||||
|
])
|
||||||
|
def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_buy_order,
|
||||||
|
amend_last, wallet, max_open, lsamr, expected) -> None:
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
fetch_ticker=ticker,
|
||||||
|
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2),
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
get_fee=fee
|
||||||
|
)
|
||||||
|
default_conf['dry_run_wallet'] = wallet
|
||||||
|
|
||||||
|
default_conf['amend_last_stake_amount'] = amend_last
|
||||||
|
default_conf['last_stake_amount_min_ratio'] = lsamr
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
|
for i in range(0, max_open):
|
||||||
|
|
||||||
|
if expected[i] is not None:
|
||||||
|
result = freqtrade.get_trade_stake_amount('ETH/BTC')
|
||||||
|
assert pytest.approx(result) == expected[i]
|
||||||
|
freqtrade.execute_buy('ETH/BTC', result)
|
||||||
|
else:
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
freqtrade.get_trade_stake_amount('ETH/BTC')
|
||||||
|
|
||||||
|
|
||||||
def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None:
|
def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5)
|
patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
||||||
freqtrade.get_trade_stake_amount('ETH/BTC')
|
freqtrade.get_trade_stake_amount('ETH/BTC')
|
||||||
|
|
||||||
|
|
||||||
def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker,
|
@pytest.mark.parametrize("balance_ratio,result1", [
|
||||||
|
(1, 0.005),
|
||||||
|
(0.99, 0.00495),
|
||||||
|
(0.50, 0.0025),
|
||||||
|
])
|
||||||
|
def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1,
|
||||||
limit_buy_order, fee, mocker) -> None:
|
limit_buy_order, fee, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
patch_wallet(mocker, free=default_conf['stake_amount'])
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
@ -164,32 +208,34 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker,
|
|||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT
|
conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT
|
||||||
|
conf['dry_run_wallet'] = 0.01
|
||||||
conf['max_open_trades'] = 2
|
conf['max_open_trades'] = 2
|
||||||
|
conf['tradable_balance_ratio'] = balance_ratio
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(conf)
|
freqtrade = FreqtradeBot(conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
# no open trades, order amount should be 'balance / max_open_trades'
|
# no open trades, order amount should be 'balance / max_open_trades'
|
||||||
result = freqtrade.get_trade_stake_amount('ETH/BTC')
|
result = freqtrade.get_trade_stake_amount('ETH/BTC')
|
||||||
assert result == default_conf['stake_amount'] / conf['max_open_trades']
|
assert result == result1
|
||||||
|
|
||||||
# create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)'
|
# create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)'
|
||||||
freqtrade.execute_buy('ETH/BTC', result)
|
freqtrade.execute_buy('ETH/BTC', result)
|
||||||
|
|
||||||
result = freqtrade.get_trade_stake_amount('LTC/BTC')
|
result = freqtrade.get_trade_stake_amount('LTC/BTC')
|
||||||
assert result == default_conf['stake_amount'] / (conf['max_open_trades'] - 1)
|
assert result == result1
|
||||||
|
|
||||||
# create 2 trades, order amount should be None
|
# create 2 trades, order amount should be None
|
||||||
freqtrade.execute_buy('LTC/BTC', result)
|
freqtrade.execute_buy('LTC/BTC', result)
|
||||||
|
|
||||||
result = freqtrade.get_trade_stake_amount('XRP/BTC')
|
result = freqtrade.get_trade_stake_amount('XRP/BTC')
|
||||||
assert result is None
|
assert result == 0
|
||||||
|
|
||||||
# set max_open_trades = None, so do not trade
|
# set max_open_trades = None, so do not trade
|
||||||
conf['max_open_trades'] = 0
|
conf['max_open_trades'] = 0
|
||||||
freqtrade = FreqtradeBot(conf)
|
freqtrade = FreqtradeBot(conf)
|
||||||
result = freqtrade.get_trade_stake_amount('NEO/BTC')
|
result = freqtrade.get_trade_stake_amount('NEO/BTC')
|
||||||
assert result is None
|
assert result == 0
|
||||||
|
|
||||||
|
|
||||||
def test_edge_called_in_process(mocker, edge_conf) -> None:
|
def test_edge_called_in_process(mocker, edge_conf) -> None:
|
||||||
@ -570,7 +616,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order,
|
|||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
assert not freqtrade.create_trade('ETH/BTC')
|
assert not freqtrade.create_trade('ETH/BTC')
|
||||||
assert freqtrade.get_trade_stake_amount('ETH/BTC') is None
|
assert freqtrade.get_trade_stake_amount('ETH/BTC') == 0
|
||||||
|
|
||||||
|
|
||||||
def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order, fee,
|
def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order, fee,
|
||||||
@ -635,11 +681,15 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("max_open", range(0, 5))
|
@pytest.mark.parametrize("max_open", range(0, 5))
|
||||||
def test_create_trades_multiple_trades(default_conf, ticker,
|
@pytest.mark.parametrize("tradable_balance_ratio,modifier", [(1.0, 1), (0.99, 0.8), (0.5, 0.5)])
|
||||||
fee, mocker, max_open) -> None:
|
def test_create_trades_multiple_trades(default_conf, ticker, fee, mocker,
|
||||||
|
max_open, tradable_balance_ratio, modifier) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
default_conf['max_open_trades'] = max_open
|
default_conf['max_open_trades'] = max_open
|
||||||
|
default_conf['tradable_balance_ratio'] = tradable_balance_ratio
|
||||||
|
default_conf['dry_run_wallet'] = 0.001 * max_open
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
@ -650,10 +700,11 @@ def test_create_trades_multiple_trades(default_conf, ticker,
|
|||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
n = freqtrade.enter_positions()
|
n = freqtrade.enter_positions()
|
||||||
assert n == max_open
|
|
||||||
|
|
||||||
trades = Trade.get_open_trades()
|
trades = Trade.get_open_trades()
|
||||||
assert len(trades) == max_open
|
# Expected trades should be max_open * a modified value
|
||||||
|
# depending on the configured tradable_balance
|
||||||
|
assert n == max(int(max_open * modifier), 0)
|
||||||
|
assert len(trades) == max(int(max_open * modifier), 0)
|
||||||
|
|
||||||
|
|
||||||
def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None:
|
def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None:
|
||||||
@ -1267,6 +1318,14 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
|||||||
rate=0.00002344 * 0.95 * 0.99,
|
rate=0.00002344 * 0.95 * 0.99,
|
||||||
stop_price=0.00002344 * 0.95)
|
stop_price=0.00002344 * 0.95)
|
||||||
|
|
||||||
|
# price fell below stoploss, so dry-run sells trade.
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
|
||||||
|
'bid': 0.00002144,
|
||||||
|
'ask': 0.00002146,
|
||||||
|
'last': 0.00002144
|
||||||
|
}))
|
||||||
|
assert freqtrade.handle_trade(trade) is True
|
||||||
|
|
||||||
|
|
||||||
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
|
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
|
||||||
limit_buy_order, limit_sell_order) -> None:
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
@ -2384,8 +2443,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
|
|||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
symbol_amount_prec=lambda s, x, y: y,
|
amount_to_precision=lambda s, x, y: y,
|
||||||
symbol_price_prec=lambda s, x, y: y,
|
price_to_precision=lambda s, x, y: y,
|
||||||
stoploss_limit=stoploss_limit,
|
stoploss_limit=stoploss_limit,
|
||||||
cancel_order=cancel_order,
|
cancel_order=cancel_order,
|
||||||
)
|
)
|
||||||
@ -2427,8 +2486,8 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
|||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
symbol_amount_prec=lambda s, x, y: y,
|
amount_to_precision=lambda s, x, y: y,
|
||||||
symbol_price_prec=lambda s, x, y: y,
|
price_to_precision=lambda s, x, y: y,
|
||||||
)
|
)
|
||||||
|
|
||||||
stoploss_limit = MagicMock(return_value={
|
stoploss_limit = MagicMock(return_value={
|
||||||
@ -2699,6 +2758,7 @@ def test__safe_sell_amount(default_conf, fee, caplog, mocker):
|
|||||||
amount = 95.33
|
amount = 95.33
|
||||||
amount_wallet = 95.29
|
amount_wallet = 95.29
|
||||||
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet))
|
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet))
|
||||||
|
wallet_update = mocker.patch('freqtrade.wallets.Wallets.update')
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='LTC/ETH',
|
pair='LTC/ETH',
|
||||||
amount=amount,
|
amount=amount,
|
||||||
@ -2711,8 +2771,15 @@ def test__safe_sell_amount(default_conf, fee, caplog, mocker):
|
|||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
wallet_update.reset_mock()
|
||||||
assert freqtrade._safe_sell_amount(trade.pair, trade.amount) == amount_wallet
|
assert freqtrade._safe_sell_amount(trade.pair, trade.amount) == amount_wallet
|
||||||
assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
||||||
|
assert wallet_update.call_count == 1
|
||||||
|
caplog.clear()
|
||||||
|
wallet_update.reset_mock()
|
||||||
|
assert freqtrade._safe_sell_amount(trade.pair, amount_wallet) == amount_wallet
|
||||||
|
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
||||||
|
assert wallet_update.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test__safe_sell_amount_error(default_conf, fee, caplog, mocker):
|
def test__safe_sell_amount_error(default_conf, fee, caplog, mocker):
|
||||||
@ -3622,6 +3689,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order,
|
|||||||
# Initialize to 2 times stake amount
|
# Initialize to 2 times stake amount
|
||||||
default_conf['dry_run_wallet'] = 0.002
|
default_conf['dry_run_wallet'] = 0.002
|
||||||
default_conf['max_open_trades'] = 2
|
default_conf['max_open_trades'] = 2
|
||||||
|
default_conf['tradable_balance_ratio'] = 1.0
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -3643,5 +3711,5 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order,
|
|||||||
n = bot.enter_positions()
|
n = bot.enter_positions()
|
||||||
assert n == 0
|
assert n == 0
|
||||||
assert log_has_re(r"Unable to create trade for XRP/BTC: "
|
assert log_has_re(r"Unable to create trade for XRP/BTC: "
|
||||||
r"Available balance \(0 BTC\) is lower than stake amount \(0.001 BTC\)",
|
r"Available balance \(0.0 BTC\) is lower than stake amount \(0.001 BTC\)",
|
||||||
caplog)
|
caplog)
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.rpc.rpc import RPC
|
||||||
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
||||||
from tests.conftest import get_patched_freqtradebot, patch_get_signal
|
from tests.conftest import get_patched_freqtradebot, patch_get_signal
|
||||||
from freqtrade.rpc.rpc import RPC
|
|
||||||
|
|
||||||
|
|
||||||
def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||||
@ -57,8 +58,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
symbol_amount_prec=lambda s, x, y: y,
|
amount_to_precision=lambda s, x, y: y,
|
||||||
symbol_price_prec=lambda s, x, y: y,
|
price_to_precision=lambda s, x, y: y,
|
||||||
get_order=stoploss_order_mock,
|
get_order=stoploss_order_mock,
|
||||||
cancel_order=cancel_order_mock,
|
cancel_order=cancel_order_mock,
|
||||||
)
|
)
|
||||||
@ -96,8 +97,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||||||
|
|
||||||
# Only order for 3rd trade needs to be cancelled
|
# Only order for 3rd trade needs to be cancelled
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
# Wallets should only be called once per sell cycle
|
# Wallets must be updated between stoploss cancellation and selling.
|
||||||
assert wallets_mock.call_count == 1
|
assert wallets_mock.call_count == 2
|
||||||
|
|
||||||
trade = trades[0]
|
trade = trades[0]
|
||||||
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
|
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
@ -112,13 +113,22 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||||||
assert not trade.is_open
|
assert not trade.is_open
|
||||||
|
|
||||||
|
|
||||||
def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, mocker) -> None:
|
@pytest.mark.parametrize("balance_ratio,result1", [
|
||||||
|
(1, 200),
|
||||||
|
(0.99, 198),
|
||||||
|
])
|
||||||
|
def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, mocker, balance_ratio,
|
||||||
|
result1) -> None:
|
||||||
"""
|
"""
|
||||||
Tests workflow
|
Tests workflow unlimited stake-amount
|
||||||
|
Buy 4 trades, forcebuy a 5th trade
|
||||||
|
Sell one trade, calculated stake amount should now be lower than before since
|
||||||
|
one trade was sold at a loss.
|
||||||
"""
|
"""
|
||||||
default_conf['max_open_trades'] = 5
|
default_conf['max_open_trades'] = 5
|
||||||
default_conf['forcebuy_enable'] = True
|
default_conf['forcebuy_enable'] = True
|
||||||
default_conf['stake_amount'] = 'unlimited'
|
default_conf['stake_amount'] = 'unlimited'
|
||||||
|
default_conf['tradable_balance_ratio'] = balance_ratio
|
||||||
default_conf['dry_run_wallet'] = 1000
|
default_conf['dry_run_wallet'] = 1000
|
||||||
default_conf['exchange']['name'] = 'binance'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
default_conf['telegram']['enabled'] = True
|
default_conf['telegram']['enabled'] = True
|
||||||
@ -127,8 +137,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
|
|||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
symbol_amount_prec=lambda s, x, y: y,
|
amount_to_precision=lambda s, x, y: y,
|
||||||
symbol_price_prec=lambda s, x, y: y,
|
price_to_precision=lambda s, x, y: y,
|
||||||
)
|
)
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -159,13 +169,15 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
|
|||||||
|
|
||||||
trades = Trade.query.all()
|
trades = Trade.query.all()
|
||||||
assert len(trades) == 4
|
assert len(trades) == 4
|
||||||
|
assert freqtrade.get_trade_stake_amount('XRP/BTC') == result1
|
||||||
|
|
||||||
rpc._rpc_forcebuy('TKN/BTC', None)
|
rpc._rpc_forcebuy('TKN/BTC', None)
|
||||||
|
|
||||||
trades = Trade.query.all()
|
trades = Trade.query.all()
|
||||||
assert len(trades) == 5
|
assert len(trades) == 5
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
assert trade.stake_amount == 200
|
assert trade.stake_amount == result1
|
||||||
# Reset trade open order id's
|
# Reset trade open order id's
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trades = Trade.get_open_trades()
|
trades = Trade.get_open_trades()
|
||||||
@ -177,6 +189,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
|
|||||||
trades = Trade.get_open_trades()
|
trades = Trade.get_open_trades()
|
||||||
# One trade sold
|
# One trade sold
|
||||||
assert len(trades) == 4
|
assert len(trades) == 4
|
||||||
|
# stake-amount should now be reduced, since one trade was sold at a loss.
|
||||||
|
assert freqtrade.get_trade_stake_amount('XRP/BTC') < result1
|
||||||
# Validate that balance of sold trade is not in dry-run balances anymore.
|
# Validate that balance of sold trade is not in dry-run balances anymore.
|
||||||
bals2 = freqtrade.wallets.get_all_balances()
|
bals2 = freqtrade.wallets.get_all_balances()
|
||||||
assert bals != bals2
|
assert bals != bals2
|
||||||
|
@ -13,6 +13,7 @@ from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
|
|||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
|
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
|
||||||
from freqtrade.plot.plotting import (add_indicators, add_profit,
|
from freqtrade.plot.plotting import (add_indicators, add_profit,
|
||||||
|
create_plotconfig,
|
||||||
generate_candlestick_graph,
|
generate_candlestick_graph,
|
||||||
generate_plot_filename,
|
generate_plot_filename,
|
||||||
generate_profit_graph, init_plotscript,
|
generate_profit_graph, init_plotscript,
|
||||||
@ -66,8 +67,8 @@ def test_add_indicators(default_conf, testdatadir, caplog):
|
|||||||
|
|
||||||
data = history.load_pair_history(pair=pair, timeframe='1m',
|
data = history.load_pair_history(pair=pair, timeframe='1m',
|
||||||
datadir=testdatadir, timerange=timerange)
|
datadir=testdatadir, timerange=timerange)
|
||||||
indicators1 = ["ema10"]
|
indicators1 = {"ema10": {}}
|
||||||
indicators2 = ["macd"]
|
indicators2 = {"macd": {"color": "red"}}
|
||||||
|
|
||||||
# Generate buy/sell signals and indicators
|
# Generate buy/sell signals and indicators
|
||||||
strat = DefaultStrategy(default_conf)
|
strat = DefaultStrategy(default_conf)
|
||||||
@ -86,9 +87,10 @@ def test_add_indicators(default_conf, testdatadir, caplog):
|
|||||||
macd = find_trace_in_fig_data(figure.data, "macd")
|
macd = find_trace_in_fig_data(figure.data, "macd")
|
||||||
assert isinstance(macd, go.Scatter)
|
assert isinstance(macd, go.Scatter)
|
||||||
assert macd.yaxis == "y3"
|
assert macd.yaxis == "y3"
|
||||||
|
assert macd.line.color == "red"
|
||||||
|
|
||||||
# No indicator found
|
# No indicator found
|
||||||
fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data)
|
fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators={'no_indicator': {}}, data=data)
|
||||||
assert fig == fig3
|
assert fig == fig3
|
||||||
assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog)
|
assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog)
|
||||||
|
|
||||||
@ -108,18 +110,29 @@ def test_plot_trades(testdatadir, caplog):
|
|||||||
figure = fig1.layout.figure
|
figure = fig1.layout.figure
|
||||||
|
|
||||||
# Check buys - color, should be in first graph, ...
|
# Check buys - color, should be in first graph, ...
|
||||||
trade_buy = find_trace_in_fig_data(figure.data, "trade_buy")
|
trade_buy = find_trace_in_fig_data(figure.data, 'Trade buy')
|
||||||
assert isinstance(trade_buy, go.Scatter)
|
assert isinstance(trade_buy, go.Scatter)
|
||||||
assert trade_buy.yaxis == 'y'
|
assert trade_buy.yaxis == 'y'
|
||||||
assert len(trades) == len(trade_buy.x)
|
assert len(trades) == len(trade_buy.x)
|
||||||
assert trade_buy.marker.color == 'green'
|
assert trade_buy.marker.color == 'cyan'
|
||||||
|
assert trade_buy.marker.symbol == 'circle-open'
|
||||||
|
assert trade_buy.text[0] == '4.0%, roi, 15 min'
|
||||||
|
|
||||||
trade_sell = find_trace_in_fig_data(figure.data, "trade_sell")
|
trade_sell = find_trace_in_fig_data(figure.data, 'Sell - Profit')
|
||||||
assert isinstance(trade_sell, go.Scatter)
|
assert isinstance(trade_sell, go.Scatter)
|
||||||
assert trade_sell.yaxis == 'y'
|
assert trade_sell.yaxis == 'y'
|
||||||
assert len(trades) == len(trade_sell.x)
|
assert len(trades.loc[trades['profitperc'] > 0]) == len(trade_sell.x)
|
||||||
assert trade_sell.marker.color == 'red'
|
assert trade_sell.marker.color == 'green'
|
||||||
assert trade_sell.text[0] == "4.0%, roi, 15 min"
|
assert trade_sell.marker.symbol == 'square-open'
|
||||||
|
assert trade_sell.text[0] == '4.0%, roi, 15 min'
|
||||||
|
|
||||||
|
trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss')
|
||||||
|
assert isinstance(trade_sell_loss, go.Scatter)
|
||||||
|
assert trade_sell_loss.yaxis == 'y'
|
||||||
|
assert len(trades.loc[trades['profitperc'] <= 0]) == len(trade_sell_loss.x)
|
||||||
|
assert trade_sell_loss.marker.color == 'red'
|
||||||
|
assert trade_sell_loss.marker.symbol == 'square-open'
|
||||||
|
assert trade_sell_loss.text[5] == '-10.4%, stop_loss, 720 min'
|
||||||
|
|
||||||
|
|
||||||
def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog):
|
def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog):
|
||||||
@ -308,7 +321,7 @@ def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
|
|||||||
"freqtrade.plot.plotting",
|
"freqtrade.plot.plotting",
|
||||||
generate_candlestick_graph=candle_mock,
|
generate_candlestick_graph=candle_mock,
|
||||||
store_plot_file=store_mock
|
store_plot_file=store_mock
|
||||||
)
|
)
|
||||||
load_and_plot_trades(default_conf)
|
load_and_plot_trades(default_conf)
|
||||||
|
|
||||||
# Both mocks should be called once per pair
|
# Both mocks should be called once per pair
|
||||||
@ -371,3 +384,47 @@ def test_plot_profit(default_conf, mocker, testdatadir, caplog):
|
|||||||
|
|
||||||
assert profit_mock.call_args_list[0][0][0] == default_conf['pairs']
|
assert profit_mock.call_args_list[0][0][0] == default_conf['pairs']
|
||||||
assert store_mock.call_args_list[0][1]['auto_open'] is True
|
assert store_mock.call_args_list[0][1]['auto_open'] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("ind1,ind2,plot_conf,exp", [
|
||||||
|
# No indicators, use plot_conf
|
||||||
|
([], [], {},
|
||||||
|
{'main_plot': {'sma': {}, 'ema3': {}, 'ema5': {}},
|
||||||
|
'subplots': {'Other': {'macd': {}, 'macdsignal': {}}}}),
|
||||||
|
# use indicators
|
||||||
|
(['sma', 'ema3'], ['macd'], {},
|
||||||
|
{'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}}),
|
||||||
|
# only main_plot - adds empty subplots
|
||||||
|
([], [], {'main_plot': {'sma': {}}},
|
||||||
|
{'main_plot': {'sma': {}}, 'subplots': {}}),
|
||||||
|
# Main and subplots
|
||||||
|
([], [], {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
|
||||||
|
{'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}),
|
||||||
|
# no main_plot, adds empty main_plot
|
||||||
|
([], [], {'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
|
||||||
|
{'main_plot': {}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}),
|
||||||
|
# indicator 1 / 2 should have prevelance
|
||||||
|
(['sma', 'ema3'], ['macd'],
|
||||||
|
{'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
|
||||||
|
{'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}}
|
||||||
|
),
|
||||||
|
# indicator 1 - overrides plot_config main_plot
|
||||||
|
(['sma', 'ema3'], [],
|
||||||
|
{'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
|
||||||
|
{'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}
|
||||||
|
),
|
||||||
|
# indicator 2 - overrides plot_config subplots
|
||||||
|
([], ['macd', 'macd_signal'],
|
||||||
|
{'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
|
||||||
|
{'main_plot': {'sma': {}}, 'subplots': {'Other': {'macd': {}, 'macd_signal': {}}}}
|
||||||
|
),
|
||||||
|
])
|
||||||
|
def test_create_plotconfig(ind1, ind2, plot_conf, exp):
|
||||||
|
|
||||||
|
res = create_plotconfig(ind1, ind2, plot_conf)
|
||||||
|
assert 'main_plot' in res
|
||||||
|
assert 'subplots' in res
|
||||||
|
assert isinstance(res['main_plot'], dict)
|
||||||
|
assert isinstance(res['subplots'], dict)
|
||||||
|
|
||||||
|
assert res == exp
|
||||||
|
@ -32,7 +32,7 @@ def test_sync_wallet_at_boot(mocker, default_conf):
|
|||||||
assert freqtrade.wallets._wallets['GAS'].used == 0.0
|
assert freqtrade.wallets._wallets['GAS'].used == 0.0
|
||||||
assert freqtrade.wallets._wallets['GAS'].total == 0.260739
|
assert freqtrade.wallets._wallets['GAS'].total == 0.260739
|
||||||
assert freqtrade.wallets.get_free('BNT') == 1.0
|
assert freqtrade.wallets.get_free('BNT') == 1.0
|
||||||
|
assert freqtrade.wallets._last_wallet_refresh > 0
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_balances=MagicMock(return_value={
|
get_balances=MagicMock(return_value={
|
||||||
@ -61,6 +61,11 @@ def test_sync_wallet_at_boot(mocker, default_conf):
|
|||||||
assert freqtrade.wallets.get_free('GAS') == 0.270739
|
assert freqtrade.wallets.get_free('GAS') == 0.270739
|
||||||
assert freqtrade.wallets.get_used('GAS') == 0.1
|
assert freqtrade.wallets.get_used('GAS') == 0.1
|
||||||
assert freqtrade.wallets.get_total('GAS') == 0.260439
|
assert freqtrade.wallets.get_total('GAS') == 0.260439
|
||||||
|
update_mock = mocker.patch('freqtrade.wallets.Wallets._update_live')
|
||||||
|
freqtrade.wallets.update(False)
|
||||||
|
assert update_mock.call_count == 0
|
||||||
|
freqtrade.wallets.update()
|
||||||
|
assert update_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_sync_wallet_missing_data(mocker, default_conf):
|
def test_sync_wallet_missing_data(mocker, default_conf):
|
||||||
|
Loading…
Reference in New Issue
Block a user