Merge branch 'develop' of https://github.com/nicolaspapp/freqtrade into feat/relative-drawdown

This commit is contained in:
Nicolas Papp 2022-04-11 14:42:10 -03:00
commit 178240aa6c
99 changed files with 1703 additions and 1148 deletions

21
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,21 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pycqa/flake8
rev: '4.0.1'
hooks:
- id: flake8
# stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.942'
hooks:
- id: mypy
# stages: [push]
- repo: https://github.com/pycqa/isort
rev: '5.10.1'
hooks:
- id: isort
name: isort (python)
# stages: [push]

View File

@ -129,6 +129,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
- `/status <trade_id>|[table]`: Lists all or specific open trades. - `/status <trade_id>|[table]`: Lists all or specific open trades.
- `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days. - `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days.
- `/forceexit <trade_id>|all`: Instantly exits the given trade (Ignoring `minimum_roi`). - `/forceexit <trade_id>|all`: Instantly exits the given trade (Ignoring `minimum_roi`).
- `/fx <trade_id>|all`: Alias to `/forceexit`
- `/performance`: Show performance of each finished trade grouped by pair - `/performance`: Show performance of each finished trade grouped by pair
- `/balance`: Show account balance per currency. - `/balance`: Show account balance per currency.
- `/daily <n>`: Shows profit or loss per day, over the last n days. - `/daily <n>`: Shows profit or loss per day, over the last n days.

View File

@ -90,7 +90,7 @@
}, },
"bot_name": "freqtrade", "bot_name": "freqtrade",
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false, "force_enter_enable": false,
"internals": { "internals": {
"process_throttle_secs": 5 "process_throttle_secs": 5
} }

View File

@ -87,7 +87,7 @@
}, },
"bot_name": "freqtrade", "bot_name": "freqtrade",
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false, "force_entry_enable": false,
"internals": { "internals": {
"process_throttle_secs": 5 "process_throttle_secs": 5
} }

View File

@ -89,7 +89,7 @@
}, },
"bot_name": "freqtrade", "bot_name": "freqtrade",
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false, "force_entry_enable": false,
"internals": { "internals": {
"process_throttle_secs": 5 "process_throttle_secs": 5
} }

View File

@ -15,10 +15,10 @@
"trailing_stop_positive": 0.005, "trailing_stop_positive": 0.005,
"trailing_stop_positive_offset": 0.0051, "trailing_stop_positive_offset": 0.0051,
"trailing_only_offset_is_reached": false, "trailing_only_offset_is_reached": false,
"use_sell_signal": true, "use_exit_signal": true,
"sell_profit_only": false, "exit_profit_only": false,
"sell_profit_offset": 0.0, "exit_profit_offset": 0.0,
"ignore_roi_if_buy_signal": false, "ignore_roi_if_entry_signal": false,
"ignore_buying_expired_candle_after": 300, "ignore_buying_expired_candle_after": 300,
"trading_mode": "spot", "trading_mode": "spot",
"margin_mode": "", "margin_mode": "",
@ -54,9 +54,9 @@
"order_types": { "order_types": {
"entry": "limit", "entry": "limit",
"exit": "limit", "exit": "limit",
"emergencyexit": "market", "emergency_exit": "market",
"forceexit": "market", "force_exit": "market",
"forceentry": "market", "force_entry": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60, "stoploss_on_exchange_interval": 60,
@ -139,21 +139,21 @@
"status": "on", "status": "on",
"warning": "on", "warning": "on",
"startup": "on", "startup": "on",
"buy": "on", "entry": "on",
"buy_fill": "on", "entry_fill": "on",
"sell": { "exit": {
"roi": "off", "roi": "off",
"emergency_sell": "off", "emergency_exit": "off",
"force_sell": "off", "force_exit": "off",
"sell_signal": "off", "exit_signal": "off",
"trailing_stop_loss": "off", "trailing_stop_loss": "off",
"stop_loss": "off", "stop_loss": "off",
"stoploss_on_exchange": "off", "stoploss_on_exchange": "off",
"custom_sell": "off" "custom_exit": "off"
}, },
"sell_fill": "on", "exit_fill": "on",
"buy_cancel": "on", "entry_cancel": "on",
"sell_cancel": "on", "exit_cancel": "on",
"protection_trigger": "off", "protection_trigger": "off",
"protection_trigger_global": "on" "protection_trigger_global": "on"
}, },
@ -174,7 +174,7 @@
"bot_name": "freqtrade", "bot_name": "freqtrade",
"db_url": "sqlite:///tradesv3.sqlite", "db_url": "sqlite:///tradesv3.sqlite",
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false, "force_entry_enable": false,
"internals": { "internals": {
"process_throttle_secs": 5, "process_throttle_secs": 5,
"heartbeat_interval": 60 "heartbeat_interval": 60
@ -182,6 +182,7 @@
"disable_dataframe_checks": false, "disable_dataframe_checks": false,
"strategy": "SampleStrategy", "strategy": "SampleStrategy",
"strategy_path": "user_data/strategies/", "strategy_path": "user_data/strategies/",
"add_config_files": [],
"dataformat_ohlcv": "json", "dataformat_ohlcv": "json",
"dataformat_trades": "jsongz" "dataformat_trades": "jsongz"
} }

View File

@ -95,7 +95,7 @@
}, },
"bot_name": "freqtrade", "bot_name": "freqtrade",
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false, "force_entry_enable": false,
"internals": { "internals": {
"process_throttle_secs": 5 "process_throttle_secs": 5
}, },

View File

@ -279,8 +279,8 @@ A backtesting result will look like that:
|:-------------------|--------:|------:|-------:|--------:| |:-------------------|--------:|------:|-------:|--------:|
| trailing_stop_loss | 205 | 150 | 0 | 55 | | trailing_stop_loss | 205 | 150 | 0 | 55 |
| stop_loss | 166 | 0 | 0 | 166 | | stop_loss | 166 | 0 | 0 | 166 |
| sell_signal | 56 | 36 | 0 | 20 | | exit_signal | 56 | 36 | 0 | 20 |
| force_sell | 2 | 0 | 0 | 2 | | force_exit | 2 | 0 | 0 | 2 |
====================================================== LEFT OPEN TRADES REPORT ====================================================== ====================================================== LEFT OPEN TRADES REPORT ======================================================
| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | | Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| |:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
@ -345,9 +345,9 @@ The column `Avg Profit %` shows the average profit for all trades made while the
The column `Tot Profit %` shows instead the total profit % in relation to the starting balance. The column `Tot Profit %` shows instead the total profit % in relation to the starting balance.
In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`. In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`.
Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set. Your strategy performance is influenced by your buy strategy, your exit strategy, and also by the `minimal_roi` and `stop_loss` you have set.
For example, if your `minimal_roi` is only `"0": 0.01` you cannot expect the bot to make more profit than 1% (because it will sell every time a trade reaches 1%). For example, if your `minimal_roi` is only `"0": 0.01` you cannot expect the bot to make more profit than 1% (because it will exit every time a trade reaches 1%).
```json ```json
"minimal_roi": { "minimal_roi": {
@ -362,11 +362,11 @@ Hence, keep in mind that your performance is an integral mix of all different el
### Exit reasons table ### Exit reasons table
The 2nd table contains a recap of exit reasons. The 2nd table contains a recap of exit reasons.
This table can tell you which area needs some additional work (e.g. all or many of the `sell_signal` trades are losses, so you should work on improving the sell signal, or consider disabling it). This table can tell you which area needs some additional work (e.g. all or many of the `exit_signal` trades are losses, so you should work on improving the exit signal, or consider disabling it).
### Left open trades table ### Left open trades table
The 3rd table contains all trades the bot had to `forceexit` at the end of the backtesting period to present you the full picture. The 3rd table contains all trades the bot had to `force_exit` at the end of the backtesting period to present you the full picture.
This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever. This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever.
These trades are also included in the first table, but are also shown separately in this table for clarity. These trades are also included in the first table, but are also shown separately in this table for clarity.
@ -492,24 +492,24 @@ Since backtesting lacks some detailed information about what happens within a ca
- Buys happen at open-price - Buys happen at open-price
- All orders are filled at the requested price (no slippage, no unfilled orders) - All orders are filled at the requested price (no slippage, no unfilled orders)
- Sell-signal sells happen at open-price of the consecutive candle - Exit-signal exits happen at open-price of the consecutive candle
- Sell-signal is favored over Stoploss, because sell-signals are assumed to trigger on candle's open - Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open
- ROI - ROI
- sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) - exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%)
- sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit - exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit
- Forcesells caused by `<N>=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) - Forceexits caused by `<N>=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
- Stoploss sells happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price - Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price
- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes - Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` exit reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes
- Low happens before high for stoploss, protecting capital first - Low happens before high for stoploss, protecting capital first
- Trailing stoploss - Trailing stoploss
- Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered) - Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered)
- On trade entry candles that trigger trailing stoploss, the "minimum offset" (`stop_positive_offset`) is assumed (instead of high) - and the stop is calculated from this point - On trade entry candles that trigger trailing stoploss, the "minimum offset" (`stop_positive_offset`) is assumed (instead of high) - and the stop is calculated from this point
- High happens first - adjusting stoploss - High happens first - adjusting stoploss
- Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly) - Low uses the adjusted stoploss (so exits with large high-low difference are backtested correctly)
- ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies - ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies
- Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used) - Exit-reason does not explain if a trade was positive or negative, just what triggered the exit (this can look odd if negative ROI values are used)
- Evaluation sequence (if multiple signals happen on the same candle) - Evaluation sequence (if multiple signals happen on the same candle)
- Sell-signal - Exit-signal
- ROI (if not stoploss) - ROI (if not stoploss)
- Stoploss - Stoploss

View File

@ -53,14 +53,63 @@ FREQTRADE__EXCHANGE__SECRET=<yourExchangeSecret>
Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
You can specify additional configuration files in `add_config_files`. Files specified in this parameter will be loaded and merged with the initial config file. The files are resolved relative to the initial configuration file.
This is similar to using multiple `--config` parameters, but simpler in usage as you don't have to specify all files for all commands.
!!! Tip "Use multiple configuration files to keep secrets secret" !!! Tip "Use multiple configuration files to keep secrets secret"
You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself. You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself.
``` json title="user_data/config.json"
"add_config_files": [
"config-private.json"
]
```
``` bash
freqtrade trade --config user_data/config.json <...>
```
The 2nd file should only specify what you intend to override.
If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`).
For one-off commands, you can also use the below syntax by specifying multiple "--config" parameters.
``` bash ``` bash
freqtrade trade --config user_data/config.json --config user_data/config-private.json <...> freqtrade trade --config user_data/config.json --config user_data/config-private.json <...>
``` ```
The 2nd file should only specify what you intend to override.
If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`). This is equivalent to the example above - but `config-private.json` is specified as cli argument.
??? Note "config collision handling"
If the same configuration setting takes place in both `config.json` and `config-import.json`, then the parent configuration wins.
In the below case, `max_open_trades` would be 3 after the merging - as the reusable "import" configuration has this key overwritten.
``` json title="user_data/config.json"
{
"max_open_trades": 3,
"stake_currency": "USDT",
"add_config_files": [
"config-import.json"
]
}
```
``` json title="user_data/config-import.json"
{
"max_open_trades": 10,
"stake_amount": "unlimited",
}
```
Resulting combined configuration:
``` json title="Result"
{
"max_open_trades": 10,
"stake_currency": "USDT",
"stake_amount": "unlimited"
}
```
## Configuration parameters ## Configuration parameters
@ -92,7 +141,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float | `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict | `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to exit a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
| `stoploss` | **Required.** Value as ratio of the stoploss used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float (as ratio) | `stoploss` | **Required.** Value as ratio of the stoploss used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float (as ratio)
| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md#trailing-stop-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Boolean | `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md#trailing-stop-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Boolean
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float | `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
@ -105,7 +154,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer | `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer | `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String | `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency sell is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer | `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#buy-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`). | `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#buy-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
| `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled). | `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled).
| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean | `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
@ -115,11 +164,11 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`). | `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
| `exit_pricing.price_last_balance` | Interpolate the exiting price. More information [below](#exit-price-without-orderbook-enabled). | `exit_pricing.price_last_balance` | Interpolate the exiting price. More information [below](#exit-price-without-orderbook-enabled).
| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean | `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
| `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to sell. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer | `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean | `use_exit_signal` | Use exit signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `sell_profit_only` | Wait until the bot reaches `sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `exit_profit_only` | Wait until the bot reaches `exit_profit_offset` before taking an exit decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `sell_profit_offset` | Sell-signal is only active above this value. Only active in combination with `sell_profit_only=True`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio) | `exit_profit_offset` | Exit-signal is only active above this value. Only active in combination with `exit_profit_only=True`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
| `ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `ignore_roi_if_entry_signal` | Do not exit if the entry signal is still active. This setting takes preference over `minimal_roi` and `use_exit_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer | `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
| `order_types` | Configure order-types depending on the action (`"entry"`, `"exit"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict | `order_types` | Configure order-types depending on the action (`"entry"`, `"exit"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
| `order_time_in_force` | Configure time in force for entry and exit orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict | `order_time_in_force` | Configure time in force for entry and exit orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
@ -150,10 +199,12 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float | `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
| `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean | `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean
| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.webhookentry` | Payload to send on entry. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookbuycancel` | Payload to send on buy order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.webhookentrycancel` | Payload to send on entry order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.webhookentryfill` | Payload to send on entry order filled. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhooksellcancel` | Payload to send on sell order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.webhookexit` | Payload to send on exit. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookexitcancel` | Payload to send on exit order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookexitfill` | Payload to send on exit order filled. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** Boolean | `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** Boolean
| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** IPv4 | `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** IPv4
@ -164,7 +215,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.<br> *Defaults to `freqtrade`*<br> **Datatype:** String | `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.<br> *Defaults to `freqtrade`*<br> **Datatype:** String
| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. <br> **Datatype:** String, SQLAlchemy connect string | `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. <br> **Datatype:** String, SQLAlchemy connect string
| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command. <br>*Defaults to `stopped`.* <br> **Datatype:** Enum, either `stopped` or `running` | `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command. <br>*Defaults to `stopped`.* <br> **Datatype:** Enum, either `stopped` or `running`
| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. <br> **Datatype:** Boolean | `force_entry_enable` | Enables the RPC Commands to force a Trade entry. More information below. <br> **Datatype:** Boolean
| `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).<br> *Defaults to `False`*. <br> **Datatype:** Boolean | `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).<br> *Defaults to `False`*. <br> **Datatype:** Boolean
| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> **Datatype:** ClassName | `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> **Datatype:** ClassName
| `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> **Datatype:** String | `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> **Datatype:** String
@ -173,6 +224,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean | `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String | `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String | `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String
| `add_config_files` | Additional config files. These files will be loaded and merged with the current config file. The files are resolved relative to the initial file.<br> *Defaults to `[]`*. <br> **Datatype:** List of strings
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String | `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String | `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean | `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
@ -196,10 +248,10 @@ Values set in the configuration file always overwrite values set in the strategy
* `order_time_in_force` * `order_time_in_force`
* `unfilledtimeout` * `unfilledtimeout`
* `disable_dataframe_checks` * `disable_dataframe_checks`
* `use_sell_signal` - `use_exit_signal`
* `sell_profit_only` * `exit_profit_only`
* `sell_profit_offset` - `exit_profit_offset`
* `ignore_roi_if_buy_signal` - `ignore_roi_if_entry_signal`
* `ignore_buying_expired_candle_after` * `ignore_buying_expired_candle_after`
* `position_adjustment_enable` * `position_adjustment_enable`
* `max_entry_position_adjustment` * `max_entry_position_adjustment`
@ -328,10 +380,10 @@ See the example below:
```json ```json
"minimal_roi": { "minimal_roi": {
"40": 0.0, # Sell after 40 minutes if the profit is not negative "40": 0.0, # Exit after 40 minutes if the profit is not negative
"30": 0.01, # Sell after 30 minutes if there is at least 1% profit "30": 0.01, # Exit after 30 minutes if there is at least 1% profit
"20": 0.02, # Sell after 20 minutes if there is at least 2% profit "20": 0.02, # Exit after 20 minutes if there is at least 2% profit
"0": 0.04 # Sell immediately if there is at least 4% profit "0": 0.04 # Exit immediately if there is at least 4% profit
}, },
``` ```
@ -340,14 +392,14 @@ This parameter can be set in either Strategy or Configuration file. If you use i
`minimal_roi` value from the strategy file. `minimal_roi` value from the strategy file.
If it is not set in either Strategy or Configuration, a default of 1000% `{"0": 10}` is used, and minimal ROI is disabled unless your trade generates 1000% profit. If it is not set in either Strategy or Configuration, a default of 1000% `{"0": 10}` is used, and minimal ROI is disabled unless your trade generates 1000% profit.
!!! Note "Special case to forcesell after a specific time" !!! Note "Special case to forceexit after a specific time"
A special case presents using `"<N>": -1` as ROI. This forces the bot to sell a trade after N Minutes, no matter if it's positive or negative, so represents a time-limited force-sell. A special case presents using `"<N>": -1` as ROI. This forces the bot to exit a trade after N Minutes, no matter if it's positive or negative, so represents a time-limited force-exit.
### Understand forcebuy_enable ### Understand force_entry_enable
The `forcebuy_enable` configuration parameter enables the usage of forcebuy commands via Telegram and REST API. The `force_entry_enable` configuration parameter enables the usage of force-enter (`/forcelong`, `/forceshort`) commands via Telegram and REST API.
For security reasons, it's disabled by default, and freqtrade will show a warning message on startup if enabled. For security reasons, it's disabled by default, and freqtrade will show a warning message on startup if enabled.
For example, you can send `/forcebuy ETH/BTC` to the bot, which will result in freqtrade buying the pair and holds it until a regular sell-signal (ROI, stoploss, /forcesell) appears. For example, you can send `/forceenter ETH/BTC` to the bot, which will result in freqtrade buying the pair and holds it until a regular exit-signal (ROI, stoploss, /forceexit) appears.
This can be dangerous with some strategies, so use with care. This can be dangerous with some strategies, so use with care.
@ -374,18 +426,17 @@ For example, if your strategy is using a 1h timeframe, and you only want to buy
### Understand order_types ### Understand order_types
The `order_types` configuration parameter maps actions (`entry`, `exit`, `stoploss`, `emergencyexit`, `forceexit`, `forceentry`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. The `order_types` configuration parameter maps actions (`entry`, `exit`, `stoploss`, `emergency_exit`, `force_exit`, `force_entry`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
This allows to buy using limit orders, sell using This allows to enter using limit orders, exit using limit-orders, and create stoplosses using market orders.
limit-orders, and create stoplosses using market orders. It also allows to set the It also allows to set the
stoploss "on exchange" which means stoploss order would be placed immediately once stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled.
the buy order is fulfilled.
`order_types` set in the configuration file overwrites values set in the strategy as a whole, so you need to configure the whole `order_types` dictionary in one place. `order_types` set in the configuration file overwrites values set in the strategy as a whole, so you need to configure the whole `order_types` dictionary in one place.
If this is configured, the following 4 values (`entry`, `exit`, `stoploss` and `stoploss_on_exchange`) need to be present, otherwise, the bot will fail to start. If this is configured, the following 4 values (`entry`, `exit`, `stoploss` and `stoploss_on_exchange`) need to be present, otherwise, the bot will fail to start.
For information on (`emergencyexit`,`forceexit`, `forceentry`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md) For information on (`emergency_exit`,`force_exit`, `force_entry`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md)
Syntax for Strategy: Syntax for Strategy:
@ -393,9 +444,9 @@ Syntax for Strategy:
order_types = { order_types = {
"entry": "limit", "entry": "limit",
"exit": "limit", "exit": "limit",
"emergencyexit": "market", "emergency_exit": "market",
"forceentry": "market", "force_entry": "market",
"forceexit": "market", "force_exit": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": False, "stoploss_on_exchange": False,
"stoploss_on_exchange_interval": 60, "stoploss_on_exchange_interval": 60,
@ -409,9 +460,9 @@ Configuration:
"order_types": { "order_types": {
"entry": "limit", "entry": "limit",
"exit": "limit", "exit": "limit",
"emergencyexit": "market", "emergency_exit": "market",
"forceentry": "market", "force_entry": "market",
"forceexit": "market", "force_exit": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60
@ -434,7 +485,7 @@ Configuration:
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order. If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order.
!!! Warning "Warning: stoploss_on_exchange failures" !!! Warning "Warning: stoploss_on_exchange failures"
If stoploss on exchange creation fails for some reason, then an "emergency exit" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencyexit` value in the `order_types` dictionary - however, this is not advised. If stoploss on exchange creation fails for some reason, then an "emergency exit" is initiated. By default, this will exit the trade using a market order. The order-type for the emergency-exit can be changed by setting the `emergency_exit` value in the `order_types` dictionary - however, this is not advised.
### Understand order_time_in_force ### Understand order_time_in_force

View File

@ -38,7 +38,7 @@ The old section of configuration parameters (`"pairlist"`) has been deprecated i
Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4, and have been removed in 2020.9. Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4, and have been removed in 2020.9.
### Using order book steps for sell price ### Using order book steps for exit price
Using `order_book_min` and `order_book_max` used to allow stepping the orderbook and trying to find the next ROI slot - trying to place sell-orders early. Using `order_book_min` and `order_book_max` used to allow stepping the orderbook and trying to find the next ROI slot - trying to place sell-orders early.
As this does however increase risk and provides no benefit, it's been removed for maintainability purposes in 2021.7. As this does however increase risk and provides no benefit, it's been removed for maintainability purposes in 2021.7.
@ -57,7 +57,20 @@ While we may drop support for the current interface sometime in the future, we w
Please follow the [Strategy migration](strategy_migration.md) guide to migrate your strategy to the new format to start using the new functionalities. Please follow the [Strategy migration](strategy_migration.md) guide to migrate your strategy to the new format to start using the new functionalities.
### webhooks - `buy_tag` has been renamed to `enter_tag` ### webhooks - changes with 2022.4
#### `buy_tag` has been renamed to `enter_tag`
This should apply only to your strategy and potentially to webhooks. This should apply only to your strategy and potentially to webhooks.
We will keep a compatibility layer for 1-2 versions (so both `buy_tag` and `enter_tag` will still work), but support for this in webhooks will disappear after that. We will keep a compatibility layer for 1-2 versions (so both `buy_tag` and `enter_tag` will still work), but support for this in webhooks will disappear after that.
#### Naming changes
Webhook terminology changed from "sell" to "exit", and from "buy" to "entry".
* `webhookbuy` -> `webhookentry`
* `webhookbuyfill` -> `webhookentryfill`
* `webhookbuycancel` -> `webhookentrycancel`
* `webhooksell` -> `webhookexit`
* `webhooksellfill` -> `webhookexitfill`
* `webhooksellcancel` -> `webhookexitcancel`

View File

@ -26,6 +26,9 @@ Alternatively (e.g. if your system is not supported by the setup.sh script), fol
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`. This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
Then install the git hook scripts by running `pre-commit install`, so your changes will be verified locally before committing.
This avoids a lot of waiting for CI already, as some basic formatting checks are done locally on your machine.
Before opening a pull request, please familiarize yourself with our [Contributing Guidelines](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md). Before opening a pull request, please familiarize yourself with our [Contributing Guidelines](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md).
### Devcontainer setup ### Devcontainer setup
@ -220,13 +223,13 @@ Protections can have 2 different ways to stop trading for a limited :
##### Protections - per pair ##### Protections - per pair
Protections that implement the per pair approach must set `has_local_stop=True`. Protections that implement the per pair approach must set `has_local_stop=True`.
The method `stop_per_pair()` will be called whenever a trade closed (sell order completed). The method `stop_per_pair()` will be called whenever a trade closed (exit order completed).
##### Protections - global protection ##### Protections - global protection
These Protections should do their evaluation across all pairs, and consequently will also lock all pairs from trading (called a global PairLock). These Protections should do their evaluation across all pairs, and consequently will also lock all pairs from trading (called a global PairLock).
Global protection must set `has_global_stop=True` to be evaluated for global stops. Global protection must set `has_global_stop=True` to be evaluated for global stops.
The method `global_stop()` will be called whenever a trade closed (sell order completed). The method `global_stop()` will be called whenever a trade closed (exit order completed).
##### Protections - calculating lock end time ##### Protections - calculating lock end time
@ -264,7 +267,7 @@ Additional tests / steps to complete:
* Check if balance shows correctly (*) * Check if balance shows correctly (*)
* Create market order (*) * Create market order (*)
* Create limit order (*) * Create limit order (*)
* Complete trade (buy + sell) (*) * Complete trade (enter + exit) (*)
* Compare result calculation between exchange and bot * Compare result calculation between exchange and bot
* Ensure fees are applied correctly (check the database against the exchange) * Ensure fees are applied correctly (check the database against the exchange)

View File

@ -96,7 +96,7 @@ Strategy arguments:
Example: Example:
``` bash ``` bash
freqtrade plot-dataframe -p BTC/ETH freqtrade plot-dataframe -p BTC/ETH --strategy AwesomeStrategy
``` ```
The `-p/--pairs` argument can be used to specify pairs you would like to plot. The `-p/--pairs` argument can be used to specify pairs you would like to plot.
@ -107,9 +107,6 @@ The `-p/--pairs` argument can be used to specify pairs you would like to plot.
Specify custom indicators. Specify custom indicators.
Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices). Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices).
!!! Tip
You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command.
``` bash ``` bash
freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --indicators1 sma ema --indicators2 macd freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --indicators1 sma ema --indicators2 macd
``` ```

View File

@ -1,5 +1,5 @@
mkdocs==1.3.0 mkdocs==1.3.0
mkdocs-material==8.2.8 mkdocs-material==8.2.9
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==9.3 pymdown-extensions==9.3
jinja2==3.1.1 jinja2==3.1.1

View File

@ -147,8 +147,8 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance. | `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
| `forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`). | `forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`). | `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `forceenter <pair> [rate]` | Instantly enters the given pair. Rate is optional. (`forcebuy_enable` must be set to True) | `forceenter <pair> [rate]` | Instantly enters the given pair. Rate is optional. (`force_entry_enable` must be set to True)
| `forceenter <pair> <side> [rate]` | Instantly longs or shorts the given pair. Rate is optional. (`forcebuy_enable` must be set to True) | `forceenter <pair> <side> [rate]` | Instantly longs or shorts the given pair. Rate is optional. (`force_entry_enable` must be set to True)
| `performance` | Show performance of each finished trade grouped by pair. | `performance` | Show performance of each finished trade grouped by pair.
| `balance` | Show account balance per currency. | `balance` | Show account balance per currency.
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7). | `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
@ -223,8 +223,8 @@ forceenter
:param side: 'long' or 'short' :param side: 'long' or 'short'
:param price: Optional - price to buy :param price: Optional - price to buy
forcesell forceexit
Force-sell a trade. Force-exit a trade.
:param tradeid: Id of the trade (can be received via status command) :param tradeid: Id of the trade (can be received via status command)

View File

@ -52,11 +52,11 @@ SELECT * FROM trades;
## Fix trade still open after a manual exit on the exchange ## Fix trade still open after a manual exit on the exchange
!!! Warning !!! Warning
Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, forceexit <tradeid> should be used to accomplish the same thing. Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, /forceexit <tradeid> should be used to accomplish the same thing.
It is strongly advised to backup your database file before making any manual changes. It is strongly advised to backup your database file before making any manual changes.
!!! Note !!! Note
This should not be necessary after /forceexit, as forceexit orders are closed automatically by the bot on the next iteration. This should not be necessary after /forceexit, as force_exit orders are closed automatically by the bot on the next iteration.
```sql ```sql
UPDATE trades UPDATE trades
@ -78,7 +78,7 @@ SET is_open=0,
close_rate=0.19638016, close_rate=0.19638016,
close_profit=0.0496, close_profit=0.0496,
close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))), close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))),
exit_reason='force_sell' exit_reason='force_exit'
WHERE id=31; WHERE id=31;
``` ```

View File

@ -17,7 +17,7 @@ Those stoploss modes can be *on exchange* or *off exchange*.
These modes can be configured with these values: These modes can be configured with these values:
``` python ``` python
'emergencyexit': 'market', 'emergency_exit': 'market',
'stoploss_on_exchange': False 'stoploss_on_exchange': False
'stoploss_on_exchange_interval': 60, 'stoploss_on_exchange_interval': 60,
'stoploss_on_exchange_limit_ratio': 0.99 'stoploss_on_exchange_limit_ratio': 0.99
@ -52,17 +52,17 @@ The bot cannot do these every 5 seconds (at each iteration), otherwise it would
So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
### forceexit ### force_exit
`forceexit` is an optional value, which defaults to the same value as `exit` and is used when sending a `/forceexit` command from Telegram or from the Rest API. `force_exit` is an optional value, which defaults to the same value as `exit` and is used when sending a `/forceexit` command from Telegram or from the Rest API.
### forceentry ### force_entry
`forceentry` is an optional value, which defaults to the same value as `entry` and is used when sending a `/forceentry` command from Telegram or from the Rest API. `force_entry` is an optional value, which defaults to the same value as `entry` and is used when sending a `/forceentry` command from Telegram or from the Rest API.
### emergencyexit ### emergency_exit
`emergencyexit` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails. `emergency_exit` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails.
The below is the default which is used if not changed in strategy or configuration file. The below is the default which is used if not changed in strategy or configuration file.
Example from strategy file: Example from strategy file:
@ -71,7 +71,7 @@ Example from strategy file:
order_types = { order_types = {
"entry": "limit", "entry": "limit",
"exit": "limit", "exit": "limit",
"emergencyexit": "market", "emergency_exit": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"stoploss_on_exchange_interval": 60, "stoploss_on_exchange_interval": 60,

View File

@ -84,16 +84,17 @@ Freqtrade will fall back to the `proposed_stake` value should your code raise an
Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed. Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed.
Allows to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need trade data to make an exit decision. Allows to define custom exit signals, indicating that specified position should be sold. This is very useful when we need to customize exit conditions for each individual trade, or if you need trade data to make an exit decision.
For example you could implement a 1:2 risk-reward ROI with `custom_exit()`. For example you could implement a 1:2 risk-reward ROI with `custom_exit()`.
Using custom_exit() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. Using `custom_exit()` signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
!!! Note !!! Note
Returning a (none-empty) `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. Returning a (none-empty) `string` or `True` from this method is equal to setting exit signal on a candle at specified time. This method is not called when exit signal is set already, or if exit signals are disabled (`use_exit_signal=False`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
`custom_exit()` will ignore `exit_profit_only`, and will always be called unless `use_exit_signal=False`, even if there is a new enter signal.
An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: An example of how we can use different indicators depending on the current profit and also exit trades that were open longer than one day:
``` python ``` python
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -365,7 +366,7 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
entry_tag: Optional[str], **kwargs) -> float: entry_tag: Optional[str], side: str, **kwargs) -> float:
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
timeframe=self.timeframe) timeframe=self.timeframe)
@ -393,7 +394,7 @@ class AwesomeStrategy(IStrategy):
!!! Warning "Backtesting" !!! Warning "Backtesting"
Custom prices are supported in backtesting (starting with 2021.12), and orders will fill if the price falls within the candle's low/high range. Custom prices are supported in backtesting (starting with 2021.12), and orders will fill if the price falls within the candle's low/high range.
Orders that don't fill immediately are subject to regular timeout handling, which happens once per (detail) candle. Orders that don't fill immediately are subject to regular timeout handling, which happens once per (detail) candle.
`custom_exit_price()` is only called for sells of type Sell_signal and Custom sell. All other sell-types will use regular backtesting prices. `custom_exit_price()` is only called for sells of type exit_signal and Custom exit. All other exit-types will use regular backtesting prices.
## Custom order timeout rules ## Custom order timeout rules
@ -564,13 +565,13 @@ class AwesomeStrategy(IStrategy):
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param exit_reason: Exit reason. :param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell'] 'exit_signal', 'force_exit', 'emergency_exit']
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange. :return bool: When True is returned, then the exit-order is placed on the exchange.
False aborts the process False aborts the process
""" """
if exit_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0: if exit_reason == 'force_exit' and trade.calc_profit_ratio(rate) < 0:
# Reject force-sells with negative profit # Reject force-sells with negative profit
# This is just a sample, please adjust to your needs # This is just a sample, please adjust to your needs
# (this does not necessarily make sense, assuming you know when you're force-selling) # (this does not necessarily make sense, assuming you know when you're force-selling)
@ -665,7 +666,7 @@ class DigDeeperStrategy(IStrategy):
if last_candle['close'] < previous_candle['close']: if last_candle['close'] < previous_candle['close']:
return None return None
filled_entries = trade.select_filled_orders(trade.enter_side) filled_entries = trade.select_filled_orders(trade.entry_side)
count_of_entries = trade.nr_of_successful_entries count_of_entries = trade.nr_of_successful_entries
# Allow up to 3 additional increasingly larger buys (4 in total) # Allow up to 3 additional increasingly larger buys (4 in total)
# Initial buy is 1x # Initial buy is 1x

View File

@ -99,7 +99,7 @@ With this section, you have a new column in your dataframe, which has `1` assign
### Customize Indicators ### Customize Indicators
Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file. Buy and sell signals need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
You should only add the indicators used in either `populate_entry_trend()`, `populate_exit_trend()`, or to populate another indicator, otherwise performance may suffer. You should only add the indicators used in either `populate_entry_trend()`, `populate_exit_trend()`, or to populate another indicator, otherwise performance may suffer.
@ -263,8 +263,8 @@ def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFram
### Exit signal rules ### Exit signal rules
Edit the method `populate_exit_trend()` into your strategy file to update your sell strategy. Edit the method `populate_exit_trend()` into your strategy file to update your exit strategy.
Please note that the sell-signal is only used if `use_sell_signal` is set to true in the configuration. Please note that the exit-signal is only used if `use_exit_signal` is set to true in the configuration.
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected. It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
@ -275,7 +275,7 @@ Sample from `user_data/strategies/sample_strategy.py`:
```python ```python
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the exit signal for the given dataframe
:param dataframe: DataFrame populated with indicators :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
@ -319,7 +319,7 @@ def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame
### Minimal ROI ### Minimal ROI
This dict defines the minimal Return On Investment (ROI) a trade should reach before selling, independent from the sell signal. This dict defines the minimal Return On Investment (ROI) a trade should reach before exiting, independent from the exit signal.
It is of the following format, with the dict key (left side of the colon) being the minutes passed since the trade opened, and the value (right side of the colon) being the percentage. It is of the following format, with the dict key (left side of the colon) being the minutes passed since the trade opened, and the value (right side of the colon) being the percentage.
@ -334,10 +334,10 @@ minimal_roi = {
The above configuration would therefore mean: The above configuration would therefore mean:
- Sell whenever 4% profit was reached - Exit whenever 4% profit was reached
- Sell when 2% profit was reached (in effect after 20 minutes) - Exit when 2% profit was reached (in effect after 20 minutes)
- Sell when 1% profit was reached (in effect after 30 minutes) - Exit when 1% profit was reached (in effect after 30 minutes)
- Sell when trade is non-loosing (in effect after 40 minutes) - Exit when trade is non-loosing (in effect after 40 minutes)
The calculation does include fees. The calculation does include fees.
@ -349,7 +349,7 @@ minimal_roi = {
} }
``` ```
While technically not completely disabled, this would sell once the trade reaches 10000% Profit. While technically not completely disabled, this would exit once the trade reaches 10000% Profit.
To use times based on candle duration (timeframe), the following snippet can be handy. To use times based on candle duration (timeframe), the following snippet can be handy.
This will allow you to change the timeframe for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...) This will allow you to change the timeframe for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...)
@ -385,7 +385,7 @@ For the full documentation on stoploss features, look at the dedicated [stoploss
This is the set of candles the bot should download and use for the analysis. This is the set of candles the bot should download and use for the analysis.
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work. Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
Please note that the same buy/sell signals may work well with one timeframe, but not with the others. Please note that the same entry/exit signals may work well with one timeframe, but not with the others.
This setting is accessible within the strategy methods as the `self.timeframe` attribute. This setting is accessible within the strategy methods as the `self.timeframe` attribute.
@ -1088,7 +1088,7 @@ The following lists some common patterns which should be avoided to prevent frus
### Colliding signals ### Colliding signals
When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are 1), freqtrade will do nothing and ignore the entry signal. This will avoid trades that buy, and sell immediately. Obviously, this can potentially lead to missed entries. When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are 1), freqtrade will do nothing and ignore the entry signal. This will avoid trades that enter, and exit immediately. Obviously, this can potentially lead to missed entries.
The following rules apply, and entry signals will be ignored if more than one of the 3 signals is set: The following rules apply, and entry signals will be ignored if more than one of the 3 signals is set:

View File

@ -9,6 +9,8 @@ You can use the quick summary as checklist. Please refer to the detailed section
## Quick summary / migration checklist ## Quick summary / migration checklist
Note : `forcesell`, `forcebuy`, `emergencysell` are changed to `force_exit`, `force_enter`, `emergency_exit` respectively.
* Strategy methods: * Strategy methods:
* [`populate_buy_trend()` -> `populate_entry_trend()`](#populate_buy_trend) * [`populate_buy_trend()` -> `populate_entry_trend()`](#populate_buy_trend)
* [`populate_sell_trend()` -> `populate_exit_trend()`](#populate_sell_trend) * [`populate_sell_trend()` -> `populate_exit_trend()`](#populate_sell_trend)
@ -18,6 +20,7 @@ You can use the quick summary as checklist. Please refer to the detailed section
* New `side` argument to callbacks without trade object * New `side` argument to callbacks without trade object
* [`custom_stake_amount`](#custom-stake-amount) * [`custom_stake_amount`](#custom-stake-amount)
* [`confirm_trade_entry`](#confirm_trade_entry) * [`confirm_trade_entry`](#confirm_trade_entry)
* [`custom_entry_price`](#custom_entry_price)
* [Changed argument name in `confirm_trade_exit`](#confirm_trade_exit) * [Changed argument name in `confirm_trade_exit`](#confirm_trade_exit)
* Dataframe columns: * Dataframe columns:
* [`buy` -> `enter_long`](#populate_buy_trend) * [`buy` -> `enter_long`](#populate_buy_trend)
@ -26,7 +29,7 @@ You can use the quick summary as checklist. Please refer to the detailed section
* [New column `enter_short` and corresponding new column `exit_short`](#populate_sell_trend) * [New column `enter_short` and corresponding new column `exit_short`](#populate_sell_trend)
* trade-object now has the following new properties: * trade-object now has the following new properties:
* `is_short` * `is_short`
* `enter_side` * `entry_side`
* `exit_side` * `exit_side`
* `trade_direction` * `trade_direction`
* renamed: `sell_reason` -> `exit_reason` * renamed: `sell_reason` -> `exit_reason`
@ -40,6 +43,32 @@ You can use the quick summary as checklist. Please refer to the detailed section
* `order_time_in_force` buy -> entry, sell -> exit. * `order_time_in_force` buy -> entry, sell -> exit.
* `order_types` buy -> entry, sell -> exit. * `order_types` buy -> entry, sell -> exit.
* `unfilledtimeout` buy -> entry, sell -> exit. * `unfilledtimeout` buy -> entry, sell -> exit.
* Terminology changes
* Sell reasons changed to reflect the new naming of "exit" instead of sells. Be careful in your strategy if you're using `exit_reason` checks and eventually update your strategy.
* `sell_signal` -> `exit_signal`
* `custom_sell` -> `custom_exit`
* `force_sell` -> `force_exit`
* `emergency_sell` -> `emergency_exit`
* Webhook terminology changed from "sell" to "exit", and from "buy" to entry
* `webhookbuy` -> `webhookentry`
* `webhookbuyfill` -> `webhookentryfill`
* `webhookbuycancel` -> `webhookentrycancel`
* `webhooksell` -> `webhookexit`
* `webhooksellfill` -> `webhookexitfill`
* `webhooksellcancel` -> `webhookexitcancel`
* Telegram notification settings
* `buy` -> `entry`
* `buy_fill` -> `entry_fill`
* `buy_cancel` -> `entry_cancel`
* `sell` -> `exit`
* `sell_fill` -> `exit_fill`
* `sell_cancel` -> `exit_cancel`
* Strategy/config settings:
* `use_sell_signal` -> `use_exit_signal`
* `sell_profit_only` -> `exit_profit_only`
* `sell_profit_offset` -> `exit_profit_offset`
* `ignore_roi_if_buy_signal` -> `ignore_roi_if_entry_signal`
* `forcebuy_enable` -> `force_entry_enable`
## Extensive explanation ## Extensive explanation
@ -116,6 +145,9 @@ Please refer to the [Strategy documentation](strategy-customization.md#exit-sign
### `custom_sell` ### `custom_sell`
`custom_sell` has been renamed to `custom_exit`.
It's now also being called for every iteration, independent of current profit and `exit_profit_only` settings.
``` python hl_lines="2" ``` python hl_lines="2"
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
@ -227,6 +259,26 @@ class AwesomeStrategy(IStrategy):
return True return True
``` ```
### `custom_entry_price`
New string argument `side` - which can be either `"long"` or `"short"`.
``` python hl_lines="3"
class AwesomeStrategy(IStrategy):
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
entry_tag: Optional[str], **kwargs) -> float:
return proposed_rate
```
After:
``` python hl_lines="3"
class AwesomeStrategy(IStrategy):
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
entry_tag: Optional[str], side: str, **kwargs) -> float:
return proposed_rate
```
### Adjust trade position changes ### Adjust trade position changes
While adjust-trade-position itself did not change, you should no longer use `trade.nr_of_successful_buys` - and instead use `trade.nr_of_successful_entries`, which will also include short entries. While adjust-trade-position itself did not change, you should no longer use `trade.nr_of_successful_buys` - and instead use `trade.nr_of_successful_entries`, which will also include short entries.
@ -288,6 +340,7 @@ After:
#### `order_types` #### `order_types`
`order_types` have changed all wordings from `buy` to `entry` - and `sell` to `exit`. `order_types` have changed all wordings from `buy` to `entry` - and `sell` to `exit`.
And two words are joined with `_`.
``` python hl_lines="2-6" ``` python hl_lines="2-6"
order_types = { order_types = {
@ -308,15 +361,40 @@ After:
order_types = { order_types = {
"entry": "limit", "entry": "limit",
"exit": "limit", "exit": "limit",
"emergencyexit": "market", "emergency_exit": "market",
"forceexit": "market", "force_exit": "market",
"forceentry": "market", "force_entry": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60
} }
``` ```
#### Strategy level settings
* `use_sell_signal` -> `use_exit_signal`
* `sell_profit_only` -> `exit_profit_only`
* `sell_profit_offset` -> `exit_profit_offset`
* `ignore_roi_if_buy_signal` -> `ignore_roi_if_entry_signal`
``` python hl_lines="2-5"
# These values can be overridden in the config.
use_sell_signal = True
sell_profit_only = True
sell_profit_offset: 0.01
ignore_roi_if_buy_signal = False
```
After:
``` python hl_lines="2-5"
# These values can be overridden in the config.
use_exit_signal = True
exit_profit_only = True
exit_profit_offset: 0.01
ignore_roi_if_entry_signal = False
```
#### `unfilledtimeout` #### `unfilledtimeout`
`unfilledtimeout` have changed all wordings from `buy` to `entry` - and `sell` to `exit`. `unfilledtimeout` have changed all wordings from `buy` to `entry` - and `sell` to `exit`.

View File

@ -81,21 +81,21 @@ Example configuration showing the different settings:
"status": "silent", "status": "silent",
"warning": "on", "warning": "on",
"startup": "off", "startup": "off",
"buy": "silent", "entry": "silent",
"sell": { "exit": {
"roi": "silent", "roi": "silent",
"emergency_sell": "on", "emergency_exit": "on",
"force_sell": "on", "force_exit": "on",
"sell_signal": "silent", "exit_signal": "silent",
"trailing_stop_loss": "on", "trailing_stop_loss": "on",
"stop_loss": "on", "stop_loss": "on",
"stoploss_on_exchange": "on", "stoploss_on_exchange": "on",
"custom_sell": "silent" "custom_exit": "silent"
}, },
"buy_cancel": "silent", "entry_cancel": "silent",
"sell_cancel": "on", "exit_cancel": "on",
"buy_fill": "off", "entry_fill": "off",
"sell_fill": "off", "exit_fill": "off",
"protection_trigger": "off", "protection_trigger": "off",
"protection_trigger_global": "on" "protection_trigger_global": "on"
}, },
@ -104,8 +104,8 @@ Example configuration showing the different settings:
}, },
``` ```
`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange. `entry` notifications are sent when the order is placed, while `entry_fill` notifications are sent when the order is filled on the exchange.
`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange. `exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
`*_fill` notifications are off by default and must be explicitly enabled. `*_fill` notifications are off by default and must be explicitly enabled.
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered. `protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
@ -173,8 +173,9 @@ official commands. You can ask at any moment for help with `/help`.
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
| `/forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`). | `/forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `/forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`). | `/forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`forcebuy_enable` must be set to True) | `/fx` | alias for `/forceexit`
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`forcebuy_enable` must be set to True) | `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
| `/performance` | Show performance of each finished trade grouped by pair | `/performance` | Show performance of each finished trade grouped by pair
| `/balance` | Show account balance per currency | `/balance` | Show account balance per currency
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7) | `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
@ -274,9 +275,12 @@ The relative profit of `1.2%` is the average profit per trade.
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`. The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
### /forcesell <trade_id> ### /forceexit <trade_id>
> **BINANCE:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)` > **BINANCE:** Exiting BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
!!! Tip
You can get a list of all open trades by calling `/forceexit` without parameter, which will show a list of buttons to simply exit a trade.
### /forcelong <pair> [rate] | /forceshort <pair> [rate] ### /forcelong <pair> [rate] | /forceshort <pair> [rate]
@ -285,13 +289,13 @@ Starting capital is either taken from the `available_capital` setting, or calcul
> **BINANCE:** Long ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`) > **BINANCE:** Long ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
Omitting the pair will open a query asking for the pair to trade (based on the current whitelist). Omitting the pair will open a query asking for the pair to trade (based on the current whitelist).
Trades crated through `/forceentry` will have the buy-tag of `forceentry`. Trades created through `/forcelong` will have the buy-tag of `force_entry`.
![Telegram force-buy screenshot](assets/telegram_forcebuy.png) ![Telegram force-buy screenshot](assets/telegram_forcebuy.png)
Note that for this to work, `forcebuy_enable` needs to be set to true. Note that for this to work, `force_entry_enable` needs to be set to true.
[More details](configuration.md#understand-forcebuy_enable) [More details](configuration.md#understand-force_entry_enable)
### /performance ### /performance

View File

@ -2,6 +2,10 @@
To update your freqtrade installation, please use one of the below methods, corresponding to your installation method. To update your freqtrade installation, please use one of the below methods, corresponding to your installation method.
!!! Note "Tracking changes"
Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release.
For the develop branch, please follow PR's to avoid being surprised by changes.
## docker-compose ## docker-compose
!!! Note "Legacy installations using the `master` image" !!! Note "Legacy installations using the `master` image"

View File

@ -10,33 +10,33 @@ Sample configuration (tested using IFTTT).
"webhook": { "webhook": {
"enabled": true, "enabled": true,
"url": "https://maker.ifttt.com/trigger/<YOUREVENT>/with/key/<YOURKEY>/", "url": "https://maker.ifttt.com/trigger/<YOUREVENT>/with/key/<YOURKEY>/",
"webhookbuy": { "webhookentry": {
"value1": "Buying {pair}", "value1": "Buying {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}" "value3": "{stake_amount:8f} {stake_currency}"
}, },
"webhookbuycancel": { "webhookentrycancel": {
"value1": "Cancelling Open Buy Order for {pair}", "value1": "Cancelling Open Buy Order for {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}" "value3": "{stake_amount:8f} {stake_currency}"
}, },
"webhookbuyfill": { "webhookentryfill": {
"value1": "Buy Order for {pair} filled", "value1": "Buy Order for {pair} filled",
"value2": "at {open_rate:8f}", "value2": "at {open_rate:8f}",
"value3": "" "value3": ""
}, },
"webhooksell": { "webhookexit": {
"value1": "Selling {pair}", "value1": "Exiting {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
}, },
"webhooksellcancel": { "webhookexitcancel": {
"value1": "Cancelling Open Sell Order for {pair}", "value1": "Cancelling Open Exit Order for {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
}, },
"webhooksellfill": { "webhookexitfill": {
"value1": "Sell Order for {pair} filled", "value1": "Exit Order for {pair} filled",
"value2": "at {close_rate:8f}.", "value2": "at {close_rate:8f}.",
"value3": "" "value3": ""
}, },
@ -96,9 +96,9 @@ Optional parameters are available to enable automatic retries for webhook messag
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called. Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
### Webhookbuy ### Webhookentry
The fields in `webhook.webhookbuy` are filled when the bot executes a long/short. Parameters are filled using string.format. The fields in `webhook.webhookentry` are filled when the bot executes a long/short. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`
@ -118,9 +118,9 @@ Possible parameters are:
* `current_rate` * `current_rate`
* `enter_tag` * `enter_tag`
### Webhookbuycancel ### Webhookentrycancel
The fields in `webhook.webhookbuycancel` are filled when the bot cancels a long/short order. Parameters are filled using string.format. The fields in `webhook.webhookentrycancel` are filled when the bot cancels a long/short order. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`
@ -139,9 +139,9 @@ Possible parameters are:
* `current_rate` * `current_rate`
* `enter_tag` * `enter_tag`
### Webhookbuyfill ### Webhookentryfill
The fields in `webhook.webhookbuyfill` are filled when the bot filled a long/short order. Parameters are filled using string.format. The fields in `webhook.webhookentryfill` are filled when the bot filled a long/short order. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`
@ -160,8 +160,9 @@ Possible parameters are:
* `current_rate` * `current_rate`
* `enter_tag` * `enter_tag`
### Webhooksell ### Webhookexit
The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format.
The fields in `webhook.webhookexit` are filled when the bot exits a trade. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`
@ -183,9 +184,9 @@ Possible parameters are:
* `open_date` * `open_date`
* `close_date` * `close_date`
### Webhooksellfill ### Webhookexitfill
The fields in `webhook.webhooksellfill` are filled when the bot fills a sell order (closes a Trae). Parameters are filled using string.format. The fields in `webhook.webhookexitfill` are filled when the bot fills a exit order (closes a Trade). Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`
@ -208,9 +209,9 @@ Possible parameters are:
* `open_date` * `open_date`
* `close_date` * `close_date`
### Webhooksellcancel ### Webhookexitcancel
The fields in `webhook.webhooksellcancel` are filled when the bot cancels a sell order. Parameters are filled using string.format. The fields in `webhook.webhookexitcancel` are filled when the bot cancels a exit order. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`

View File

@ -202,6 +202,8 @@ def ask_user_config() -> Dict[str, Any]:
if not answers: if not answers:
# Interrupted questionary sessions return an empty dict. # Interrupted questionary sessions return an empty dict.
raise OperationalException("User interrupted interactive questions.") raise OperationalException("User interrupted interactive questions.")
# Ensure default is set for non-futures exchanges
answers['trading_mode'] = answers.get('trading_mode', "spot")
answers['margin_mode'] = ( answers['margin_mode'] = (
'isolated' 'isolated'
if answers.get('trading_mode') == 'futures' if answers.get('trading_mode') == 'futures'

View File

@ -154,9 +154,9 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
if not conf.get('edge', {}).get('enabled'): if not conf.get('edge', {}).get('enabled'):
return return
if not conf.get('use_sell_signal', True): if not conf.get('use_exit_signal', True):
raise OperationalException( raise OperationalException(
"Edge requires `use_sell_signal` to be True, otherwise no sells will happen." "Edge requires `use_exit_signal` to be True, otherwise no sells will happen."
) )
@ -219,6 +219,7 @@ def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
_validate_order_types(conf) _validate_order_types(conf)
_validate_unfilledtimeout(conf) _validate_unfilledtimeout(conf)
_validate_pricing_rules(conf) _validate_pricing_rules(conf)
_strategy_settings(conf)
def _validate_time_in_force(conf: Dict[str, Any]) -> None: def _validate_time_in_force(conf: Dict[str, Any]) -> None:
@ -243,7 +244,9 @@ def _validate_time_in_force(conf: Dict[str, Any]) -> None:
def _validate_order_types(conf: Dict[str, Any]) -> None: def _validate_order_types(conf: Dict[str, Any]) -> None:
order_types = conf.get('order_types', {}) order_types = conf.get('order_types', {})
if any(x in order_types for x in ['buy', 'sell', 'emergencysell', 'forcebuy', 'forcesell']): old_order_types = ['buy', 'sell', 'emergencysell', 'forcebuy',
'forcesell', 'emergencyexit', 'forceexit', 'forceentry']
if any(x in order_types for x in old_order_types):
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
raise OperationalException( raise OperationalException(
"Please migrate your order_types settings to use the new wording.") "Please migrate your order_types settings to use the new wording.")
@ -255,9 +258,12 @@ def _validate_order_types(conf: Dict[str, Any]) -> None:
for o, n in [ for o, n in [
('buy', 'entry'), ('buy', 'entry'),
('sell', 'exit'), ('sell', 'exit'),
('emergencysell', 'emergencyexit'), ('emergencysell', 'emergency_exit'),
('forcesell', 'forceexit'), ('forcesell', 'force_exit'),
('forcebuy', 'forceentry'), ('forcebuy', 'force_entry'),
('emergencyexit', 'emergency_exit'),
('forceexit', 'force_exit'),
('forceentry', 'force_entry'),
]: ]:
process_deprecated_setting(conf, 'order_types', o, 'order_types', n) process_deprecated_setting(conf, 'order_types', o, 'order_types', n)
@ -312,3 +318,12 @@ def _validate_pricing_rules(conf: Dict[str, Any]) -> None:
else: else:
process_deprecated_setting(conf, 'ask_strategy', obj, 'exit_pricing', obj) process_deprecated_setting(conf, 'ask_strategy', obj, 'exit_pricing', obj)
del conf['ask_strategy'] del conf['ask_strategy']
def _strategy_settings(conf: Dict[str, Any]) -> None:
process_deprecated_setting(conf, None, 'use_sell_signal', None, 'use_exit_signal')
process_deprecated_setting(conf, None, 'sell_profit_only', None, 'exit_profit_only')
process_deprecated_setting(conf, None, 'sell_profit_offset', None, 'exit_profit_offset')
process_deprecated_setting(conf, None, 'ignore_roi_if_buy_signal',
None, 'ignore_roi_if_entry_signal')

View File

@ -12,7 +12,7 @@ from freqtrade.configuration.check_exchange import check_exchange
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
from freqtrade.configuration.environment_vars import enironment_vars_to_dict from freqtrade.configuration.environment_vars import enironment_vars_to_dict
from freqtrade.configuration.load_config import load_config_file, load_file from freqtrade.configuration.load_config import load_file, load_from_files
from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, CandleType, RunMode, TradingMode from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, CandleType, RunMode, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.loggers import setup_logging from freqtrade.loggers import setup_logging
@ -55,45 +55,28 @@ class Configuration:
:param files: List of file paths :param files: List of file paths
:return: configuration dictionary :return: configuration dictionary
""" """
# Keep this method as staticmethod, so it can be used from interactive environments
c = Configuration({'config': files}, RunMode.OTHER) c = Configuration({'config': files}, RunMode.OTHER)
return c.get_config() return c.get_config()
def load_from_files(self, files: List[str]) -> Dict[str, Any]:
# Keep this method as staticmethod, so it can be used from interactive environments
config: Dict[str, Any] = {}
if not files:
return deepcopy(constants.MINIMAL_CONFIG)
# We expect here a list of config filenames
for path in files:
logger.info(f'Using config: {path} ...')
# Merge config options, overwriting old values
config = deep_merge_dicts(load_config_file(path), config)
# Load environment variables
env_data = enironment_vars_to_dict()
config = deep_merge_dicts(env_data, config)
config['config_files'] = files
# Normalize config
if 'internals' not in config:
config['internals'] = {}
if 'pairlists' not in config:
config['pairlists'] = []
return config
def load_config(self) -> Dict[str, Any]: def load_config(self) -> Dict[str, Any]:
""" """
Extract information for sys.argv and load the bot configuration Extract information for sys.argv and load the bot configuration
:return: Configuration dictionary :return: Configuration dictionary
""" """
# Load all configs # Load all configs
config: Dict[str, Any] = self.load_from_files(self.args.get("config", [])) config: Dict[str, Any] = load_from_files(self.args.get("config", []))
# Load environment variables
env_data = enironment_vars_to_dict()
config = deep_merge_dicts(env_data, config)
# Normalize config
if 'internals' not in config:
config['internals'] = {}
if 'pairlists' not in config:
config['pairlists'] = []
# Keep a copy of the original configuration file # Keep a copy of the original configuration file
config['original_config'] = deepcopy(config) config['original_config'] = deepcopy(config)
@ -164,8 +147,8 @@ class Configuration:
config.update({'db_url': self.args['db_url']}) config.update({'db_url': self.args['db_url']})
logger.info('Parameter --db-url detected ...') logger.info('Parameter --db-url detected ...')
if config.get('forcebuy_enable', False): if config.get('force_entry_enable', False):
logger.warning('`forcebuy` RPC message enabled.') logger.warning('`force_entry_enable` RPC message enabled.')
# Support for sd_notify # Support for sd_notify
if 'sd_notify' in self.args and self.args['sd_notify']: if 'sd_notify' in self.args and self.args['sd_notify']:
@ -433,8 +416,9 @@ class Configuration:
logstring='Detected --new-pairs-days: {}') logstring='Detected --new-pairs-days: {}')
self._args_to_config(config, argname='trading_mode', self._args_to_config(config, argname='trading_mode',
logstring='Detected --trading-mode: {}') logstring='Detected --trading-mode: {}')
config['candle_type_def'] = CandleType.get_default(config.get('trading_mode', 'spot')) config['candle_type_def'] = CandleType.get_default(
config['trading_mode'] = TradingMode(config.get('trading_mode', 'spot')) config.get('trading_mode', 'spot') or 'spot')
config['trading_mode'] = TradingMode(config.get('trading_mode', 'spot') or 'spot')
self._args_to_config(config, argname='candle_types', self._args_to_config(config, argname='candle_types',
logstring='Detected --candle-types: {}') logstring='Detected --candle-types: {}')

View File

@ -12,14 +12,15 @@ logger = logging.getLogger(__name__)
def check_conflicting_settings(config: Dict[str, Any], def check_conflicting_settings(config: Dict[str, Any],
section_old: str, name_old: str, section_old: Optional[str], name_old: str,
section_new: Optional[str], name_new: str) -> None: section_new: Optional[str], name_new: str) -> None:
section_new_config = config.get(section_new, {}) if section_new else config section_new_config = config.get(section_new, {}) if section_new else config
section_old_config = config.get(section_old, {}) section_old_config = config.get(section_old, {}) if section_old else config
if name_new in section_new_config and name_old in section_old_config: if name_new in section_new_config and name_old in section_old_config:
new_name = f"{section_new}.{name_new}" if section_new else f"{name_new}" new_name = f"{section_new}.{name_new}" if section_new else f"{name_new}"
old_name = f"{section_old}.{name_old}" if section_old else f"{name_old}"
raise OperationalException( raise OperationalException(
f"Conflicting settings `{new_name}` and `{section_old}.{name_old}` " f"Conflicting settings `{new_name}` and `{old_name}` "
"(DEPRECATED) detected in the configuration file. " "(DEPRECATED) detected in the configuration file. "
"This deprecated setting will be removed in the next versions of Freqtrade. " "This deprecated setting will be removed in the next versions of Freqtrade. "
f"Please delete it from your configuration and use the `{new_name}` " f"Please delete it from your configuration and use the `{new_name}` "
@ -47,17 +48,18 @@ def process_removed_setting(config: Dict[str, Any],
def process_deprecated_setting(config: Dict[str, Any], def process_deprecated_setting(config: Dict[str, Any],
section_old: str, name_old: str, section_old: Optional[str], name_old: str,
section_new: Optional[str], name_new: str section_new: Optional[str], name_new: str
) -> None: ) -> None:
check_conflicting_settings(config, section_old, name_old, section_new, name_new) check_conflicting_settings(config, section_old, name_old, section_new, name_new)
section_old_config = config.get(section_old, {}) section_old_config = config.get(section_old, {}) if section_old else config
if name_old in section_old_config: if name_old in section_old_config:
section_1 = f"{section_old}.{name_old}" if section_old else f"{name_old}"
section_2 = f"{section_new}.{name_new}" if section_new else f"{name_new}" section_2 = f"{section_new}.{name_new}" if section_new else f"{name_new}"
logger.warning( logger.warning(
"DEPRECATED: " "DEPRECATED: "
f"The `{section_old}.{name_old}` setting is deprecated and " f"The `{section_1}` setting is deprecated and "
"will be removed in the next versions of Freqtrade. " "will be removed in the next versions of Freqtrade. "
f"Please use the `{section_2}` setting in your configuration instead." f"Please use the `{section_2}` setting in your configuration instead."
) )
@ -72,25 +74,51 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
# Kept for future deprecated / moved settings # Kept for future deprecated / moved settings
# check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal', # check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal',
# 'experimental', 'use_sell_signal') # 'experimental', 'use_sell_signal')
process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal',
None, 'use_sell_signal')
process_deprecated_setting(config, 'ask_strategy', 'sell_profit_only',
None, 'sell_profit_only')
process_deprecated_setting(config, 'ask_strategy', 'sell_profit_offset',
None, 'sell_profit_offset')
process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
None, 'ignore_roi_if_buy_signal')
process_deprecated_setting(config, 'ask_strategy', 'ignore_buying_expired_candle_after', process_deprecated_setting(config, 'ask_strategy', 'ignore_buying_expired_candle_after',
None, 'ignore_buying_expired_candle_after') None, 'ignore_buying_expired_candle_after')
# Legacy way - having them in experimental ... process_deprecated_setting(config, None, 'forcebuy_enable', None, 'force_entry_enable')
process_removed_setting(config, 'experimental', 'use_sell_signal',
None, 'use_sell_signal')
process_removed_setting(config, 'experimental', 'sell_profit_only',
None, 'sell_profit_only')
process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal',
None, 'ignore_roi_if_buy_signal')
# New settings
if config.get('telegram'):
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell',
'notification_settings', 'exit')
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_fill',
'notification_settings', 'exit_fill')
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_cancel',
'notification_settings', 'exit_cancel')
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy',
'notification_settings', 'entry')
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy_fill',
'notification_settings', 'entry_fill')
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy_cancel',
'notification_settings', 'entry_cancel')
if config.get('webhook'):
process_deprecated_setting(config, 'webhook', 'webhookbuy', 'webhook', 'webhookentry')
process_deprecated_setting(config, 'webhook', 'webhookbuycancel',
'webhook', 'webhookentrycancel')
process_deprecated_setting(config, 'webhook', 'webhookbuyfill',
'webhook', 'webhookentryfill')
process_deprecated_setting(config, 'webhook', 'webhooksell', 'webhook', 'webhookexit')
process_deprecated_setting(config, 'webhook', 'webhooksellcancel',
'webhook', 'webhookexitcancel')
process_deprecated_setting(config, 'webhook', 'webhooksellfill',
'webhook', 'webhookexitfill')
# Legacy way - having them in experimental ...
process_removed_setting(config, 'experimental', 'use_sell_signal', None, 'use_exit_signal')
process_removed_setting(config, 'experimental', 'sell_profit_only', None, 'exit_profit_only')
process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal',
None, 'ignore_roi_if_entry_signal')
process_removed_setting(config, 'ask_strategy', 'use_sell_signal', None, 'exit_sell_signal')
process_removed_setting(config, 'ask_strategy', 'sell_profit_only', None, 'exit_profit_only')
process_removed_setting(config, 'ask_strategy', 'sell_profit_offset',
None, 'exit_profit_offset')
process_removed_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
None, 'ignore_roi_if_entry_signal')
if (config.get('edge', {}).get('enabled', False) if (config.get('edge', {}).get('enabled', False)
and 'capital_available_percentage' in config.get('edge', {})): and 'capital_available_percentage' in config.get('edge', {})):
raise OperationalException( raise OperationalException(

View File

@ -4,12 +4,15 @@ This module contain functions to load the configuration file
import logging import logging
import re import re
import sys import sys
from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict, List
import rapidjson import rapidjson
from freqtrade.constants import MINIMAL_CONFIG
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import deep_merge_dicts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -70,3 +73,43 @@ def load_config_file(path: str) -> Dict[str, Any]:
) )
return config return config
def load_from_files(files: List[str], base_path: Path = None, level: int = 0) -> Dict[str, Any]:
"""
Recursively load configuration files if specified.
Sub-files are assumed to be relative to the initial config.
"""
config: Dict[str, Any] = {}
if level > 5:
raise OperationalException("Config loop detected.")
if not files:
return deepcopy(MINIMAL_CONFIG)
files_loaded = []
# We expect here a list of config filenames
for filename in files:
logger.info(f'Using config: {filename} ...')
if filename == '-':
# Immediately load stdin and return
return load_config_file(filename)
file = Path(filename)
if base_path:
# Prepend basepath to allow for relative assignments
file = base_path / file
config_tmp = load_config_file(str(file))
if 'add_config_files' in config_tmp:
config_sub = load_from_files(
config_tmp['add_config_files'], file.resolve().parent, level + 1)
files_loaded.extend(config_sub.get('config_files', []))
config_tmp = deep_merge_dicts(config_tmp, config_sub)
files_loaded.insert(0, str(file))
# Merge config options, overwriting prior values
config = deep_merge_dicts(config_tmp, config)
config['config_files'] = files_loaded
return config

View File

@ -3,7 +3,7 @@
""" """
bot constants bot constants
""" """
from typing import List, Tuple from typing import List, Literal, Tuple
from freqtrade.enums import CandleType from freqtrade.enums import CandleType
@ -87,20 +87,19 @@ 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",
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD", "RUB", "UAH", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR",
"BTC", "ETH", "XRP", "LTC", "BCH" "USD", "BTC", "ETH", "XRP", "LTC", "BCH"
] ]
MINIMAL_CONFIG = { MINIMAL_CONFIG = {
'stake_currency': '', "stake_currency": "",
'dry_run': True, "dry_run": True,
'exchange': { "exchange": {
'name': '', "name": "",
'key': '', "key": "",
'secret': '', "secret": "",
'pair_whitelist': [], "pair_whitelist": [],
'ccxt_async_config': { "ccxt_async_config": {
'enableRateLimit': True,
} }
} }
} }
@ -150,10 +149,10 @@ CONF_SCHEMA = {
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
'trailing_only_offset_is_reached': {'type': 'boolean'}, 'trailing_only_offset_is_reached': {'type': 'boolean'},
'use_sell_signal': {'type': 'boolean'}, 'use_exit_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'}, 'exit_profit_only': {'type': 'boolean'},
'sell_profit_offset': {'type': 'number'}, 'exit_profit_offset': {'type': 'number'},
'ignore_roi_if_buy_signal': {'type': 'boolean'}, 'ignore_roi_if_entry_signal': {'type': 'boolean'},
'ignore_buying_expired_candle_after': {'type': 'number'}, 'ignore_buying_expired_candle_after': {'type': 'number'},
'trading_mode': {'type': 'string', 'enum': TRADING_MODES}, 'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
'margin_mode': {'type': 'string', 'enum': MARGIN_MODES}, 'margin_mode': {'type': 'string', 'enum': MARGIN_MODES},
@ -217,9 +216,9 @@ CONF_SCHEMA = {
'properties': { 'properties': {
'entry': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'entry': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'exit': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'exit': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'forceexit': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'force_exit': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'forceentry': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'force_entry': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'emergencyexit': { 'emergency_exit': {
'type': 'string', 'type': 'string',
'enum': ORDERTYPE_POSSIBILITIES, 'enum': ORDERTYPE_POSSIBILITIES,
'default': 'market'}, 'default': 'market'},
@ -286,21 +285,21 @@ CONF_SCHEMA = {
'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'entry': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'entry_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'buy_fill': {'type': 'string', 'entry_fill': {'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS, 'enum': TELEGRAM_SETTING_OPTIONS,
'default': 'off' 'default': 'off'
}, },
'sell': { 'exit': {
'type': ['string', 'object'], 'type': ['string', 'object'],
'additionalProperties': { 'additionalProperties': {
'type': 'string', 'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS 'enum': TELEGRAM_SETTING_OPTIONS
} }
}, },
'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'exit_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'sell_fill': { 'exit_fill': {
'type': 'string', 'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS, 'enum': TELEGRAM_SETTING_OPTIONS,
'default': 'off' 'default': 'off'
@ -328,12 +327,12 @@ CONF_SCHEMA = {
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'}, 'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
'retries': {'type': 'integer', 'minimum': 0}, 'retries': {'type': 'integer', 'minimum': 0},
'retry_delay': {'type': 'number', 'minimum': 0}, 'retry_delay': {'type': 'number', 'minimum': 0},
'webhookbuy': {'type': 'object'}, 'webhookentry': {'type': 'object'},
'webhookbuycancel': {'type': 'object'}, 'webhookentrycancel': {'type': 'object'},
'webhookbuyfill': {'type': 'object'}, 'webhookentryfill': {'type': 'object'},
'webhooksell': {'type': 'object'}, 'webhookexit': {'type': 'object'},
'webhooksellcancel': {'type': 'object'}, 'webhookexitcancel': {'type': 'object'},
'webhooksellfill': {'type': 'object'}, 'webhookexitfill': {'type': 'object'},
'webhookstatus': {'type': 'object'}, 'webhookstatus': {'type': 'object'},
}, },
}, },
@ -359,7 +358,7 @@ CONF_SCHEMA = {
'export': {'type': 'string', 'enum': EXPORT_OPTIONS, 'default': 'trades'}, 'export': {'type': 'string', 'enum': EXPORT_OPTIONS, 'default': 'trades'},
'disableparamexport': {'type': 'boolean'}, 'disableparamexport': {'type': 'boolean'},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'forcebuy_enable': {'type': 'boolean'}, 'force_entry_enable': {'type': 'boolean'},
'disable_dataframe_checks': {'type': 'boolean'}, 'disable_dataframe_checks': {'type': 'boolean'},
'internals': { 'internals': {
'type': 'object', 'type': 'object',
@ -479,7 +478,7 @@ CANCEL_REASON = {
"FULLY_CANCELLED": "fully cancelled", "FULLY_CANCELLED": "fully cancelled",
"ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)", "ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)",
"CANCELLED_ON_EXCHANGE": "cancelled on exchange", "CANCELLED_ON_EXCHANGE": "cancelled on exchange",
"FORCE_SELL": "forcesold", "FORCE_EXIT": "forcesold",
} }
# List of pairs with their timeframes # List of pairs with their timeframes
@ -488,3 +487,6 @@ ListPairsWithTimeframes = List[PairWithTimeframe]
# Type for trades list # Type for trades list
TradeList = List[List] TradeList = List[List]
LongShort = Literal['long', 'short']
EntryExit = Literal['entry', 'exit']

View File

@ -193,14 +193,7 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
continue continue
if min_backtest_date is not None: if min_backtest_date is not None:
try:
backtest_date = strategy_metadata['backtest_start_time'] backtest_date = strategy_metadata['backtest_start_time']
except KeyError:
# TODO: this can be removed starting from feb 2022
# The metadata-file without start_time was only available in develop
# and was never included in an official release.
# Older metadata format without backtest time, too old to consider.
return results
backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc) backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc)
if backtest_date < min_backtest_date: if backtest_date < min_backtest_date:
# Do not use a cached result for this strategy as first result is too old. # Do not use a cached result for this strategy as first result is too old.

View File

@ -179,6 +179,7 @@ def _download_pair_history(pair: str, *,
data_handler: IDataHandler = None, data_handler: IDataHandler = None,
timerange: Optional[TimeRange] = None, timerange: Optional[TimeRange] = None,
candle_type: CandleType, candle_type: CandleType,
erase: bool = False,
) -> bool: ) -> bool:
""" """
Download latest candles from the exchange for the pair and timeframe passed in parameters Download latest candles from the exchange for the pair and timeframe passed in parameters
@ -192,11 +193,16 @@ def _download_pair_history(pair: str, *,
:param timeframe: Timeframe (e.g "5m") :param timeframe: Timeframe (e.g "5m")
:param timerange: range of time to download :param timerange: range of time to download
:param candle_type: Any of the enum CandleType (must match trading mode!) :param candle_type: Any of the enum CandleType (must match trading mode!)
:param erase: Erase existing data
:return: bool with success state :return: bool with success state
""" """
data_handler = get_datahandler(datadir, data_handler=data_handler) data_handler = get_datahandler(datadir, data_handler=data_handler)
try: try:
if erase:
if data_handler.ohlcv_purge(pair, timeframe, candle_type=candle_type):
logger.info(f'Deleting existing data for pair {pair}, {timeframe}, {candle_type}.')
logger.info( logger.info(
f'Download history data for pair: "{pair}" ({process}), timeframe: {timeframe}, ' f'Download history data for pair: "{pair}" ({process}), timeframe: {timeframe}, '
f'candle type: {candle_type} and store in {datadir}.' f'candle type: {candle_type} and store in {datadir}.'
@ -267,35 +273,28 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
continue continue
for timeframe in timeframes: for timeframe in timeframes:
if erase:
if data_handler.ohlcv_purge(pair, timeframe, candle_type=candle_type):
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.')
logger.info(f'Downloading pair {pair}, interval {timeframe}.') logger.info(f'Downloading pair {pair}, interval {timeframe}.')
process = f'{idx}/{len(pairs)}' process = f'{idx}/{len(pairs)}'
_download_pair_history(pair=pair, process=process, _download_pair_history(pair=pair, process=process,
datadir=datadir, exchange=exchange, datadir=datadir, exchange=exchange,
timerange=timerange, data_handler=data_handler, timerange=timerange, data_handler=data_handler,
timeframe=str(timeframe), new_pairs_days=new_pairs_days, timeframe=str(timeframe), new_pairs_days=new_pairs_days,
candle_type=candle_type) candle_type=candle_type,
erase=erase)
if trading_mode == 'futures': if trading_mode == 'futures':
# Predefined candletype (and timeframe) depending on exchange # Predefined candletype (and timeframe) depending on exchange
# Downloads what is necessary to backtest based on futures data. # Downloads what is necessary to backtest based on futures data.
timeframe = exchange._ft_has['mark_ohlcv_timeframe'] tf_mark = exchange._ft_has['mark_ohlcv_timeframe']
fr_candle_type = CandleType.from_string(exchange._ft_has['mark_ohlcv_price']) fr_candle_type = CandleType.from_string(exchange._ft_has['mark_ohlcv_price'])
# All exchanges need FundingRate for futures trading. # All exchanges need FundingRate for futures trading.
# The timeframe is aligned to the mark-price timeframe. # The timeframe is aligned to the mark-price timeframe.
for funding_candle_type in (CandleType.FUNDING_RATE, fr_candle_type): for funding_candle_type in (CandleType.FUNDING_RATE, fr_candle_type):
# TODO: this could be in most parts to the above.
if erase:
if data_handler.ohlcv_purge(pair, timeframe, candle_type=funding_candle_type):
logger.info(
f'Deleting existing data for pair {pair}, interval {timeframe}.')
_download_pair_history(pair=pair, process=process, _download_pair_history(pair=pair, process=process,
datadir=datadir, exchange=exchange, datadir=datadir, exchange=exchange,
timerange=timerange, data_handler=data_handler, timerange=timerange, data_handler=data_handler,
timeframe=str(timeframe), new_pairs_days=new_pairs_days, timeframe=str(tf_mark), new_pairs_days=new_pairs_days,
candle_type=funding_candle_type) candle_type=funding_candle_type,
erase=erase)
return pairs_not_available return pairs_not_available

View File

@ -470,7 +470,7 @@ class Edge:
if len(ohlc_columns) - 1 < exit_index: if len(ohlc_columns) - 1 < exit_index:
break break
exit_type = ExitType.SELL_SIGNAL exit_type = ExitType.EXIT_SIGNAL
exit_price = ohlc_columns[exit_index, 0] exit_price = ohlc_columns[exit_index, 0]
trade = {'pair': pair, trade = {'pair': pair,

View File

@ -3,16 +3,16 @@ from enum import Enum
class ExitType(Enum): class ExitType(Enum):
""" """
Enum to distinguish between sell reasons Enum to distinguish between exit reasons
""" """
ROI = "roi" ROI = "roi"
STOP_LOSS = "stop_loss" STOP_LOSS = "stop_loss"
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange" STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
TRAILING_STOP_LOSS = "trailing_stop_loss" TRAILING_STOP_LOSS = "trailing_stop_loss"
SELL_SIGNAL = "sell_signal" EXIT_SIGNAL = "exit_signal"
FORCE_SELL = "force_sell" FORCE_EXIT = "force_exit"
EMERGENCY_SELL = "emergency_sell" EMERGENCY_EXIT = "emergency_exit"
CUSTOM_SELL = "custom_sell" CUSTOM_EXIT = "custom_exit"
NONE = "" NONE = ""
def __str__(self): def __str__(self):

View File

@ -6,19 +6,13 @@ class RPCMessageType(Enum):
WARNING = 'warning' WARNING = 'warning'
STARTUP = 'startup' STARTUP = 'startup'
BUY = 'buy' ENTRY = 'entry'
BUY_FILL = 'buy_fill' ENTRY_FILL = 'entry_fill'
BUY_CANCEL = 'buy_cancel' ENTRY_CANCEL = 'entry_cancel'
SHORT = 'short' EXIT = 'exit'
SHORT_FILL = 'short_fill' EXIT_FILL = 'exit_fill'
SHORT_CANCEL = 'short_cancel' EXIT_CANCEL = 'exit_cancel'
# TODO: The below messagetypes should be renamed to "exit"!
# Careful - has an impact on webhooks, therefore needs proper communication
SELL = 'sell'
SELL_FILL = 'sell_fill'
SELL_CANCEL = 'sell_cancel'
PROTECTION_TRIGGER = 'protection_trigger' PROTECTION_TRIGGER = 'protection_trigger'
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global' PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'

View File

@ -20,7 +20,7 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRU
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES,
ListPairsWithTimeframes, PairWithTimeframe) EntryExit, ListPairsWithTimeframes, PairWithTimeframe)
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
@ -341,15 +341,11 @@ class Exchange:
return sorted(set([x['quote'] for _, x in markets.items()])) return sorted(set([x['quote'] for _, x in markets.items()]))
def get_pair_quote_currency(self, pair: str) -> str: def get_pair_quote_currency(self, pair: str) -> str:
""" """ Return a pair's quote currency (base/quote:settlement) """
Return a pair's quote currency
"""
return self.markets.get(pair, {}).get('quote', '') return self.markets.get(pair, {}).get('quote', '')
def get_pair_base_currency(self, pair: str) -> str: def get_pair_base_currency(self, pair: str) -> str:
""" """ Return a pair's base currency (base/quote:settlement) """
Return a pair's base currency
"""
return self.markets.get(pair, {}).get('base', '') return self.markets.get(pair, {}).get('base', '')
def market_is_future(self, market: Dict[str, Any]) -> bool: def market_is_future(self, market: Dict[str, Any]) -> bool:
@ -1429,7 +1425,7 @@ class Exchange:
raise OperationalException(e) from e raise OperationalException(e) from e
def get_rate(self, pair: str, refresh: bool, def get_rate(self, pair: str, refresh: bool,
side: Literal['entry', 'exit'], is_short: bool) -> float: side: EntryExit, is_short: bool) -> float:
""" """
Calculates bid/ask target Calculates bid/ask target
bid rate - between current ask price and last price bid rate - between current ask price and last price

View File

@ -7,12 +7,13 @@ import traceback
from datetime import datetime, time, timezone from datetime import datetime, time, timezone
from math import isclose from math import isclose
from threading import Lock from threading import Lock
from typing import Any, Dict, List, Literal, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from schedule import Scheduler from schedule import Scheduler
from freqtrade import __version__, constants from freqtrade import __version__, constants
from freqtrade.configuration import validate_config_consistency from freqtrade.configuration import validate_config_consistency
from freqtrade.constants import LongShort
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
@ -190,7 +191,7 @@ class FreqtradeBot(LoggingMixin):
# Check and handle any timed out open orders # Check and handle any timed out open orders
self.check_handle_timedout() self.check_handle_timedout()
# Protect from collisions with forceexit. # Protect from collisions with force_exit.
# Without this, freqtrade my try to recreate stoploss_on_exchange orders # Without this, freqtrade my try to recreate stoploss_on_exchange orders
# while exiting is in process, since telegram messages arrive in an different thread. # while exiting is in process, since telegram messages arrive in an different thread.
with self._exit_lock: with self._exit_lock:
@ -329,12 +330,12 @@ class FreqtradeBot(LoggingMixin):
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
for trade in trades: for trade in trades:
if trade.is_open and not trade.fee_updated(trade.enter_side): if trade.is_open and not trade.fee_updated(trade.entry_side):
order = trade.select_order(trade.enter_side, False) order = trade.select_order(trade.entry_side, False)
open_order = trade.select_order(trade.enter_side, True) open_order = trade.select_order(trade.entry_side, True)
if order and open_order is None: if order and open_order is None:
logger.info( logger.info(
f"Updating {trade.enter_side}-fee on trade {trade}" f"Updating {trade.entry_side}-fee on trade {trade}"
f"for order {order.order_id}." f"for order {order.order_id}."
) )
self.update_trade_state(trade, order.order_id, send_msg=False) self.update_trade_state(trade, order.order_id, send_msg=False)
@ -363,7 +364,7 @@ class FreqtradeBot(LoggingMixin):
if fo and fo['status'] == 'open': if fo and fo['status'] == 'open':
# Assume this as the open order # Assume this as the open order
trade.open_order_id = order.order_id trade.open_order_id = order.order_id
elif order.ft_order_side == trade.enter_side: elif order.ft_order_side == trade.entry_side:
if fo and fo['status'] == 'open': if fo and fo['status'] == 'open':
trade.open_order_id = order.order_id trade.open_order_id = order.order_id
if fo: if fo:
@ -548,9 +549,9 @@ class FreqtradeBot(LoggingMixin):
order_book_bids = order_book_data_frame['b_size'].sum() order_book_bids = order_book_data_frame['b_size'].sum()
order_book_asks = order_book_data_frame['a_size'].sum() order_book_asks = order_book_data_frame['a_size'].sum()
enter_side = order_book_bids if side == SignalDirection.LONG else order_book_asks entry_side = order_book_bids if side == SignalDirection.LONG else order_book_asks
exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids
bids_ask_delta = enter_side / exit_side bids_ask_delta = entry_side / exit_side
bids = f"Bids: {order_book_bids}" bids = f"Bids: {order_book_bids}"
asks = f"Asks: {order_book_asks}" asks = f"Asks: {order_book_asks}"
@ -590,13 +591,14 @@ class FreqtradeBot(LoggingMixin):
time_in_force = self.strategy.order_time_in_force['entry'] time_in_force = self.strategy.order_time_in_force['entry']
[side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long'] [side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long']
trade_side: Literal['long', 'short'] = 'short' if is_short else 'long' trade_side: LongShort = 'short' if is_short else 'long'
pos_adjust = trade is not None pos_adjust = trade is not None
enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
pair, price, stake_amount, trade_side, enter_tag, trade) pair, price, stake_amount, trade_side, enter_tag, trade)
if not stake_amount: if not stake_amount:
logger.info(f"No stake amount to enter a trade for {pair}.")
return False return False
if pos_adjust: if pos_adjust:
@ -674,6 +676,7 @@ class FreqtradeBot(LoggingMixin):
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
base_currency = self.exchange.get_pair_base_currency(pair)
open_date = datetime.now(timezone.utc) open_date = datetime.now(timezone.utc)
funding_fees = self.exchange.get_funding_fees( funding_fees = self.exchange.get_funding_fees(
pair=pair, amount=amount, is_short=is_short, open_date=open_date) pair=pair, amount=amount, is_short=is_short, open_date=open_date)
@ -681,6 +684,8 @@ class FreqtradeBot(LoggingMixin):
if trade is None: if trade is None:
trade = Trade( trade = Trade(
pair=pair, pair=pair,
base_currency=base_currency,
stake_currency=self.config['stake_currency'],
stake_amount=stake_amount, stake_amount=stake_amount,
amount=amount, amount=amount,
is_open=True, is_open=True,
@ -746,7 +751,7 @@ class FreqtradeBot(LoggingMixin):
def get_valid_enter_price_and_stake( def get_valid_enter_price_and_stake(
self, pair: str, price: Optional[float], stake_amount: float, self, pair: str, price: Optional[float], stake_amount: float,
trade_side: Literal['long', 'short'], trade_side: LongShort,
entry_tag: Optional[str], entry_tag: Optional[str],
trade: Optional[Trade] trade: Optional[Trade]
) -> Tuple[float, float, float]: ) -> Tuple[float, float, float]:
@ -760,7 +765,9 @@ class FreqtradeBot(LoggingMixin):
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=proposed_enter_rate)( default_retval=proposed_enter_rate)(
pair=pair, current_time=datetime.now(timezone.utc), pair=pair, current_time=datetime.now(timezone.utc),
proposed_rate=proposed_enter_rate, entry_tag=entry_tag) proposed_rate=proposed_enter_rate, entry_tag=entry_tag,
side=trade_side,
)
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
@ -816,10 +823,7 @@ class FreqtradeBot(LoggingMixin):
""" """
Sends rpc notification when a entry order occurred. Sends rpc notification when a entry order occurred.
""" """
if fill: msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY
msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL
else:
msg_type = RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY
open_rate = safe_value_fallback(order, 'average', 'price') open_rate = safe_value_fallback(order, 'average', 'price')
if open_rate is None: if open_rate is None:
open_rate = trade.open_rate open_rate = trade.open_rate
@ -858,10 +862,10 @@ class FreqtradeBot(LoggingMixin):
""" """
current_rate = self.exchange.get_rate( current_rate = self.exchange.get_rate(
trade.pair, side='entry', is_short=trade.is_short, refresh=False) trade.pair, side='entry', is_short=trade.is_short, refresh=False)
msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL
msg = { msg = {
'trade_id': trade.id, 'trade_id': trade.id,
'type': msg_type, 'type': RPCMessageType.ENTRY_CANCEL,
'buy_tag': trade.enter_tag, 'buy_tag': trade.enter_tag,
'enter_tag': trade.enter_tag, 'enter_tag': trade.enter_tag,
'exchange': self.exchange.name.capitalize(), 'exchange': self.exchange.name.capitalize(),
@ -926,8 +930,8 @@ class FreqtradeBot(LoggingMixin):
exit_tag = None exit_tag = None
exit_signal_type = "exit_short" if trade.is_short else "exit_long" exit_signal_type = "exit_short" if trade.is_short else "exit_long"
if (self.config.get('use_sell_signal', True) or if (self.config.get('use_exit_signal', True) or
self.config.get('ignore_roi_if_buy_signal', False)): self.config.get('ignore_roi_if_entry_signal', False)):
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
self.strategy.timeframe) self.strategy.timeframe)
@ -978,7 +982,7 @@ class FreqtradeBot(LoggingMixin):
logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Exiting the trade forcefully') logger.warning('Exiting the trade forcefully')
self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple( self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple(
exit_type=ExitType.EMERGENCY_SELL)) exit_type=ExitType.EMERGENCY_EXIT))
except ExchangeError: except ExchangeError:
trade.stoploss_order_id = None trade.stoploss_order_id = None
@ -1136,7 +1140,7 @@ class FreqtradeBot(LoggingMixin):
continue continue
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
is_entering = order['side'] == trade.enter_side is_entering = order['side'] == trade.entry_side
not_closed = order['status'] == 'open' or fully_cancelled not_closed = order['status'] == 'open' or fully_cancelled
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
@ -1159,7 +1163,7 @@ class FreqtradeBot(LoggingMixin):
try: try:
self.execute_trade_exit( self.execute_trade_exit(
trade, order.get('price'), trade, order.get('price'),
exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_SELL)) exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT))
except DependencyException as exception: except DependencyException as exception:
logger.warning( logger.warning(
f'Unable to emergency sell trade {trade.pair}: {exception}') f'Unable to emergency sell trade {trade.pair}: {exception}')
@ -1177,7 +1181,7 @@ class FreqtradeBot(LoggingMixin):
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue continue
if order['side'] == trade.enter_side: if order['side'] == trade.entry_side:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
elif order['side'] == trade.exit_side: elif order['side'] == trade.exit_side:
@ -1216,7 +1220,7 @@ class FreqtradeBot(LoggingMixin):
corder = order corder = order
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
side = trade.enter_side.capitalize() side = trade.entry_side.capitalize()
logger.info('%s order %s for %s.', side, reason, trade) logger.info('%s order %s for %s.', side, reason, trade)
# Using filled to determine the filled amount # Using filled to determine the filled amount
@ -1247,7 +1251,7 @@ class FreqtradeBot(LoggingMixin):
self.update_trade_state(trade, trade.open_order_id, corder) self.update_trade_state(trade, trade.open_order_id, corder)
trade.open_order_id = None trade.open_order_id = None
logger.info(f'Partial {trade.enter_side} order timeout for {trade}.') logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
self.wallets.update() self.wallets.update()
@ -1377,9 +1381,9 @@ class FreqtradeBot(LoggingMixin):
trade = self.cancel_stoploss_on_exchange(trade) trade = self.cancel_stoploss_on_exchange(trade)
order_type = ordertype or self.strategy.order_types[exit_type] order_type = ordertype or self.strategy.order_types[exit_type]
if exit_check.exit_type == ExitType.EMERGENCY_SELL: if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
# Emergency sells (default to market!) # Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergencyexit", "market") order_type = self.strategy.order_types.get("emergency_exit", "market")
amount = self._safe_exit_amount(trade.pair, trade.amount) amount = self._safe_exit_amount(trade.pair, trade.amount)
time_in_force = self.strategy.order_time_in_force['exit'] time_in_force = self.strategy.order_time_in_force['exit']
@ -1414,7 +1418,7 @@ class FreqtradeBot(LoggingMixin):
trade.orders.append(order_obj) trade.orders.append(order_obj)
trade.open_order_id = order['id'] trade.open_order_id = order['id']
trade.sell_order_status = '' trade.exit_order_status = ''
trade.close_rate_requested = limit trade.close_rate_requested = limit
trade.exit_reason = exit_tag or exit_check.exit_reason trade.exit_reason = exit_tag or exit_check.exit_reason
@ -1443,8 +1447,8 @@ class FreqtradeBot(LoggingMixin):
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"
msg = { msg = {
'type': (RPCMessageType.SELL_FILL if fill 'type': (RPCMessageType.EXIT_FILL if fill
else RPCMessageType.SELL), else RPCMessageType.EXIT),
'trade_id': trade.id, 'trade_id': trade.id,
'exchange': trade.exchange.capitalize(), 'exchange': trade.exchange.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
@ -1481,10 +1485,10 @@ class FreqtradeBot(LoggingMixin):
""" """
Sends rpc notification when a sell cancel occurred. Sends rpc notification when a sell cancel occurred.
""" """
if trade.sell_order_status == reason: if trade.exit_order_status == reason:
return return
else: else:
trade.sell_order_status = reason trade.exit_order_status = reason
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
@ -1494,7 +1498,7 @@ class FreqtradeBot(LoggingMixin):
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"
msg = { msg = {
'type': RPCMessageType.SELL_CANCEL, 'type': RPCMessageType.EXIT_CANCEL,
'trade_id': trade.id, 'trade_id': trade.id,
'exchange': trade.exchange.capitalize(), 'exchange': trade.exchange.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
@ -1577,7 +1581,7 @@ class FreqtradeBot(LoggingMixin):
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES: if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
# If a entry order was closed, force update on stoploss on exchange # If a entry order was closed, force update on stoploss on exchange
if order.get('side', None) == trade.enter_side: if order.get('side', None) == trade.entry_side:
trade = self.cancel_stoploss_on_exchange(trade) trade = self.cancel_stoploss_on_exchange(trade)
# TODO: Margin will need to use interest_rate as well. # TODO: Margin will need to use interest_rate as well.
# interest_rate = self.exchange.get_interest_rate() # interest_rate = self.exchange.get_interest_rate()

View File

@ -14,7 +14,7 @@ from pandas import DataFrame
from freqtrade import constants from freqtrade import constants
from freqtrade.configuration import TimeRange, validate_config_consistency from freqtrade.configuration import TimeRange, validate_config_consistency
from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.constants import DATETIME_PRINT_FORMAT, LongShort
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.converter import trim_dataframe, trim_dataframes
@ -349,20 +349,20 @@ class Backtesting:
data[pair] = df_analyzed[headers].values.tolist() if not df_analyzed.empty else [] data[pair] = df_analyzed[headers].values.tolist() if not df_analyzed.empty else []
return data return data
def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple, def _get_close_rate(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
trade_dur: int) -> float: trade_dur: int) -> float:
""" """
Get close rate for backtesting result Get close rate for backtesting result
""" """
# Special handling if high or low hit STOP_LOSS or ROI # Special handling if high or low hit STOP_LOSS or ROI
if sell.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): if sell.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
return self._get_close_rate_for_stoploss(sell_row, trade, sell, trade_dur) return self._get_close_rate_for_stoploss(row, trade, sell, trade_dur)
elif sell.exit_type == (ExitType.ROI): elif sell.exit_type == (ExitType.ROI):
return self._get_close_rate_for_roi(sell_row, trade, sell, trade_dur) return self._get_close_rate_for_roi(row, trade, sell, trade_dur)
else: else:
return sell_row[OPEN_IDX] return row[OPEN_IDX]
def _get_close_rate_for_stoploss(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple, def _get_close_rate_for_stoploss(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
trade_dur: int) -> float: trade_dur: int) -> float:
# our stoploss was already lower than candle high, # our stoploss was already lower than candle high,
# possibly due to a cancelled trade exit. # possibly due to a cancelled trade exit.
@ -371,11 +371,11 @@ class Backtesting:
leverage = trade.leverage or 1.0 leverage = trade.leverage or 1.0
side_1 = -1 if is_short else 1 side_1 = -1 if is_short else 1
if is_short: if is_short:
if trade.stop_loss < sell_row[LOW_IDX]: if trade.stop_loss < row[LOW_IDX]:
return sell_row[OPEN_IDX] return row[OPEN_IDX]
else: else:
if trade.stop_loss > sell_row[HIGH_IDX]: if trade.stop_loss > row[HIGH_IDX]:
return sell_row[OPEN_IDX] return row[OPEN_IDX]
# Special case: trailing triggers within same candle as trade opened. Assume most # Special case: trailing triggers within same candle as trade opened. Assume most
# pessimistic price movement, which is moving just enough to arm stoploss and # pessimistic price movement, which is moving just enough to arm stoploss and
@ -388,29 +388,28 @@ class Backtesting:
and self.strategy.trailing_stop_positive and self.strategy.trailing_stop_positive
): ):
# Worst case: price reaches stop_positive_offset and dives down. # Worst case: price reaches stop_positive_offset and dives down.
stop_rate = (sell_row[OPEN_IDX] * stop_rate = (row[OPEN_IDX] *
(1 + side_1 * abs(self.strategy.trailing_stop_positive_offset) - (1 + side_1 * abs(self.strategy.trailing_stop_positive_offset) -
side_1 * abs(self.strategy.trailing_stop_positive / leverage))) side_1 * abs(self.strategy.trailing_stop_positive / leverage)))
else: else:
# Worst case: price ticks tiny bit above open and dives down. # Worst case: price ticks tiny bit above open and dives down.
stop_rate = sell_row[OPEN_IDX] * (1 - stop_rate = row[OPEN_IDX] * (1 - side_1 * abs(trade.stop_loss_pct / leverage))
side_1 * abs(trade.stop_loss_pct / leverage))
if is_short: if is_short:
assert stop_rate > sell_row[LOW_IDX] assert stop_rate > row[LOW_IDX]
else: else:
assert stop_rate < sell_row[HIGH_IDX] assert stop_rate < row[HIGH_IDX]
# Limit lower-end to candle low to avoid sells below the low. # Limit lower-end to candle low to avoid sells below the low.
# This still remains "worst case" - but "worst realistic case". # This still remains "worst case" - but "worst realistic case".
if is_short: if is_short:
return min(sell_row[HIGH_IDX], stop_rate) return min(row[HIGH_IDX], stop_rate)
else: else:
return max(sell_row[LOW_IDX], stop_rate) return max(row[LOW_IDX], stop_rate)
# Set close_rate to stoploss # Set close_rate to stoploss
return trade.stop_loss return trade.stop_loss
def _get_close_rate_for_roi(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple, def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
trade_dur: int) -> float: trade_dur: int) -> float:
is_short = trade.is_short or False is_short = trade.is_short or False
leverage = trade.leverage or 1.0 leverage = trade.leverage or 1.0
@ -418,41 +417,41 @@ class Backtesting:
roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur)
if roi is not None and roi_entry is not None: if roi is not None and roi_entry is not None:
if roi == -1 and roi_entry % self.timeframe_min == 0: if roi == -1 and roi_entry % self.timeframe_min == 0:
# When forceselling with ROI=-1, the roi time will always be equal to trade_dur. # When force_exiting with ROI=-1, the roi time will always be equal to trade_dur.
# If that entry is a multiple of the timeframe (so on candle open) # If that entry is a multiple of the timeframe (so on candle open)
# - we'll use open instead of close # - we'll use open instead of close
return sell_row[OPEN_IDX] return row[OPEN_IDX]
# - (Expected abs profit - open_rate - open_fee) / (fee_close -1) # - (Expected abs profit - open_rate - open_fee) / (fee_close -1)
roi_rate = trade.open_rate * roi / leverage roi_rate = trade.open_rate * roi / leverage
open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open) open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open)
close_rate = -(roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1) close_rate = -(roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1)
if is_short: if is_short:
is_new_roi = sell_row[OPEN_IDX] < close_rate is_new_roi = row[OPEN_IDX] < close_rate
else: else:
is_new_roi = sell_row[OPEN_IDX] > close_rate is_new_roi = row[OPEN_IDX] > close_rate
if (trade_dur > 0 and trade_dur == roi_entry if (trade_dur > 0 and trade_dur == roi_entry
and roi_entry % self.timeframe_min == 0 and roi_entry % self.timeframe_min == 0
and is_new_roi): and is_new_roi):
# new ROI entry came into effect. # new ROI entry came into effect.
# use Open rate if open_rate > calculated sell rate # use Open rate if open_rate > calculated sell rate
return sell_row[OPEN_IDX] return row[OPEN_IDX]
if (trade_dur == 0 and ( if (trade_dur == 0 and (
( (
is_short is_short
# Red candle (for longs) # Red candle (for longs)
and sell_row[OPEN_IDX] < sell_row[CLOSE_IDX] # Red candle and row[OPEN_IDX] < row[CLOSE_IDX] # Red candle
and trade.open_rate > sell_row[OPEN_IDX] # trade-open above open_rate and trade.open_rate > row[OPEN_IDX] # trade-open above open_rate
and close_rate < sell_row[CLOSE_IDX] # closes below close and close_rate < row[CLOSE_IDX] # closes below close
) )
or or
( (
not is_short not is_short
# green candle (for shorts) # green candle (for shorts)
and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # green candle and row[OPEN_IDX] > row[CLOSE_IDX] # green candle
and trade.open_rate < sell_row[OPEN_IDX] # trade-open below open_rate and trade.open_rate < row[OPEN_IDX] # trade-open below open_rate
and close_rate > sell_row[CLOSE_IDX] # closes above close and close_rate > row[CLOSE_IDX] # closes above close
) )
)): )):
# ROI on opening candles with custom pricing can only # ROI on opening candles with custom pricing can only
@ -464,11 +463,11 @@ class Backtesting:
# Use the maximum between close_rate and low as we # Use the maximum between close_rate and low as we
# cannot sell outside of a candle. # cannot sell outside of a candle.
# Applies when a new ROI setting comes in place and the whole candle is above that. # Applies when a new ROI setting comes in place and the whole candle is above that.
return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) return min(max(close_rate, row[LOW_IDX]), row[HIGH_IDX])
else: else:
# This should not be reached... # This should not be reached...
return sell_row[OPEN_IDX] return row[OPEN_IDX]
def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple
) -> LocalTrade: ) -> LocalTrade:
@ -498,7 +497,7 @@ class Backtesting:
return row[LOW_IDX] <= rate <= row[HIGH_IDX] return row[LOW_IDX] <= rate <= row[HIGH_IDX]
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
sell_row: Tuple) -> Optional[LocalTrade]: row: Tuple) -> Optional[LocalTrade]:
# Check if we need to adjust our current positions # Check if we need to adjust our current positions
if self.strategy.position_adjustment_enable: if self.strategy.position_adjustment_enable:
@ -507,15 +506,15 @@ class Backtesting:
entry_count = trade.nr_of_successful_entries entry_count = trade.nr_of_successful_entries
check_adjust_entry = (entry_count <= self.strategy.max_entry_position_adjustment) check_adjust_entry = (entry_count <= self.strategy.max_entry_position_adjustment)
if check_adjust_entry: if check_adjust_entry:
trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) trade = self._get_adjust_trade_entry_for_candle(trade, row)
sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime() sell_candle_time: datetime = row[DATE_IDX].to_pydatetime()
enter = sell_row[SHORT_IDX] if trade.is_short else sell_row[LONG_IDX] enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
exit_ = sell_row[ESHORT_IDX] if trade.is_short else sell_row[ELONG_IDX] exit_ = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
sell = self.strategy.should_exit( sell = self.strategy.should_exit(
trade, sell_row[OPEN_IDX], sell_candle_time, # type: ignore trade, row[OPEN_IDX], sell_candle_time, # type: ignore
enter=enter, exit_=exit_, enter=enter, exit_=exit_,
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX] low=row[LOW_IDX], high=row[HIGH_IDX]
) )
if sell.exit_flag: if sell.exit_flag:
@ -523,13 +522,13 @@ class Backtesting:
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
try: try:
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) closerate = self._get_close_rate(row, trade, sell, trade_dur)
except ValueError: except ValueError:
return None return None
# call the custom exit price,with default value as previous closerate # call the custom exit price,with default value as previous closerate
current_profit = trade.calc_profit_ratio(closerate) current_profit = trade.calc_profit_ratio(closerate)
order_type = self.strategy.order_types['exit'] order_type = self.strategy.order_types['exit']
if sell.exit_type in (ExitType.SELL_SIGNAL, ExitType.CUSTOM_SELL): if sell.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT):
# Custom exit pricing only for sell-signals # Custom exit pricing only for sell-signals
if order_type == 'limit': if order_type == 'limit':
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price, closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
@ -540,9 +539,9 @@ class Backtesting:
# We can't place orders lower than current low. # We can't place orders lower than current low.
# freqtrade does not support this in live, and the order would fill immediately # freqtrade does not support this in live, and the order would fill immediately
if trade.is_short: if trade.is_short:
closerate = min(closerate, sell_row[HIGH_IDX]) closerate = min(closerate, row[HIGH_IDX])
else: else:
closerate = max(closerate, sell_row[LOW_IDX]) closerate = max(closerate, row[LOW_IDX])
# Confirm trade exit: # Confirm trade exit:
time_in_force = self.strategy.order_time_in_force['exit'] time_in_force = self.strategy.order_time_in_force['exit']
@ -558,13 +557,13 @@ class Backtesting:
trade.exit_reason = sell.exit_reason trade.exit_reason = sell.exit_reason
# Checks and adds an exit tag, after checking that the length of the # Checks and adds an exit tag, after checking that the length of the
# sell_row has the length for an exit tag column # row has the length for an exit tag column
if( if(
len(sell_row) > EXIT_TAG_IDX len(row) > EXIT_TAG_IDX
and sell_row[EXIT_TAG_IDX] is not None and row[EXIT_TAG_IDX] is not None
and len(sell_row[EXIT_TAG_IDX]) > 0 and len(row[EXIT_TAG_IDX]) > 0
): ):
trade.exit_reason = sell_row[EXIT_TAG_IDX] trade.exit_reason = row[EXIT_TAG_IDX]
self.order_id_counter += 1 self.order_id_counter += 1
order = Order( order = Order(
@ -592,8 +591,8 @@ class Backtesting:
return None return None
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: def _get_sell_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime() sell_candle_time: datetime = row[DATE_IDX].to_pydatetime()
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
trade.funding_fees = self.exchange.calculate_funding_fees( trade.funding_fees = self.exchange.calculate_funding_fees(
@ -614,13 +613,13 @@ class Backtesting:
].copy() ].copy()
if len(detail_data) == 0: if len(detail_data) == 0:
# Fall back to "regular" data if no detail data was found for this candle # Fall back to "regular" data if no detail data was found for this candle
return self._get_sell_trade_entry_for_candle(trade, sell_row) return self._get_sell_trade_entry_for_candle(trade, row)
detail_data.loc[:, 'enter_long'] = sell_row[LONG_IDX] detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
detail_data.loc[:, 'exit_long'] = sell_row[ELONG_IDX] detail_data.loc[:, 'exit_long'] = row[ELONG_IDX]
detail_data.loc[:, 'enter_short'] = sell_row[SHORT_IDX] detail_data.loc[:, 'enter_short'] = row[SHORT_IDX]
detail_data.loc[:, 'exit_short'] = sell_row[ESHORT_IDX] detail_data.loc[:, 'exit_short'] = row[ESHORT_IDX]
detail_data.loc[:, 'enter_tag'] = sell_row[ENTER_TAG_IDX] detail_data.loc[:, 'enter_tag'] = row[ENTER_TAG_IDX]
detail_data.loc[:, 'exit_tag'] = sell_row[EXIT_TAG_IDX] detail_data.loc[:, 'exit_tag'] = row[EXIT_TAG_IDX]
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short', 'enter_tag', 'exit_tag'] 'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
for det_row in detail_data[headers].values.tolist(): for det_row in detail_data[headers].values.tolist():
@ -631,11 +630,11 @@ class Backtesting:
return None return None
else: else:
return self._get_sell_trade_entry_for_candle(trade, sell_row) return self._get_sell_trade_entry_for_candle(trade, row)
def get_valid_price_and_stake( def get_valid_price_and_stake(
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float], self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
direction: str, current_time: datetime, entry_tag: Optional[str], direction: LongShort, current_time: datetime, entry_tag: Optional[str],
trade: Optional[LocalTrade], order_type: str trade: Optional[LocalTrade], order_type: str
) -> Tuple[float, float, float, float]: ) -> Tuple[float, float, float, float]:
@ -643,7 +642,9 @@ class Backtesting:
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=propose_rate)( default_retval=propose_rate)(
pair=pair, current_time=current_time, pair=pair, current_time=current_time,
proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate proposed_rate=propose_rate, entry_tag=entry_tag,
side=direction,
) # default value is the open rate
# We can't place orders higher than current high (otherwise it'd be a stop limit buy) # We can't place orders higher than current high (otherwise it'd be a stop limit buy)
# which freqtrade does not support in live. # which freqtrade does not support in live.
if direction == "short": if direction == "short":
@ -694,7 +695,7 @@ class Backtesting:
return propose_rate, stake_amount_val, leverage, min_stake_amount return propose_rate, stake_amount_val, leverage, min_stake_amount
def _enter_trade(self, pair: str, row: Tuple, direction: str, def _enter_trade(self, pair: str, row: Tuple, direction: LongShort,
stake_amount: Optional[float] = None, stake_amount: Optional[float] = None,
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]: trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
@ -725,6 +726,7 @@ class Backtesting:
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
self.order_id_counter += 1 self.order_id_counter += 1
base_currency = self.exchange.get_pair_base_currency(pair)
amount = round((stake_amount / propose_rate) * leverage, 8) amount = round((stake_amount / propose_rate) * leverage, 8)
is_short = (direction == 'short') is_short = (direction == 'short')
# Necessary for Margin trading. Disabled until support is enabled. # Necessary for Margin trading. Disabled until support is enabled.
@ -737,6 +739,8 @@ class Backtesting:
id=self.trade_id_counter, id=self.trade_id_counter,
open_order_id=self.order_id_counter, open_order_id=self.order_id_counter,
pair=pair, pair=pair,
base_currency=base_currency,
stake_currency=self.config['stake_currency'],
open_rate=propose_rate, open_rate=propose_rate,
open_rate_requested=propose_rate, open_rate_requested=propose_rate,
open_date=current_time, open_date=current_time,
@ -772,8 +776,8 @@ class Backtesting:
ft_pair=trade.pair, ft_pair=trade.pair,
order_id=str(self.order_id_counter), order_id=str(self.order_id_counter),
symbol=trade.pair, symbol=trade.pair,
ft_order_side=trade.enter_side, ft_order_side=trade.entry_side,
side=trade.enter_side, side=trade.entry_side,
order_type=order_type, order_type=order_type,
status="open", status="open",
order_date=current_time, order_date=current_time,
@ -810,7 +814,7 @@ class Backtesting:
sell_row = data[pair][-1] sell_row = data[pair][-1]
trade.close_date = sell_row[DATE_IDX].to_pydatetime() trade.close_date = sell_row[DATE_IDX].to_pydatetime()
trade.exit_reason = ExitType.FORCE_SELL.value trade.exit_reason = ExitType.FORCE_EXIT.value
trade.close(sell_row[OPEN_IDX], show_msg=False) trade.close(sell_row[OPEN_IDX], show_msg=False)
LocalTrade.close_bt_trade(trade) LocalTrade.close_bt_trade(trade)
# Deepcopy object to have wallets update correctly # Deepcopy object to have wallets update correctly
@ -827,7 +831,7 @@ class Backtesting:
self.rejected_trades += 1 self.rejected_trades += 1
return False return False
def check_for_trade_entry(self, row) -> Optional[str]: def check_for_trade_entry(self, row) -> Optional[LongShort]:
enter_long = row[LONG_IDX] == 1 enter_long = row[LONG_IDX] == 1
exit_long = row[ELONG_IDX] == 1 exit_long = row[ELONG_IDX] == 1
enter_short = self._can_short and row[SHORT_IDX] == 1 enter_short = self._can_short and row[SHORT_IDX] == 1
@ -855,7 +859,7 @@ class Backtesting:
timedout = self.strategy.ft_check_timed_out(trade, order, current_time) timedout = self.strategy.ft_check_timed_out(trade, order, current_time)
if timedout: if timedout:
if order.side == trade.enter_side: if order.side == trade.entry_side:
self.timedout_entry_orders += 1 self.timedout_entry_orders += 1
if trade.nr_of_successful_entries == 0: if trade.nr_of_successful_entries == 0:
# Remove trade due to entry timeout expiration. # Remove trade due to entry timeout expiration.
@ -970,7 +974,7 @@ class Backtesting:
for trade in list(open_trades[pair]): for trade in list(open_trades[pair]):
# 3. Process entry orders. # 3. Process entry orders.
order = trade.select_order(trade.enter_side, is_open=True) order = trade.select_order(trade.entry_side, is_open=True)
if order and self._get_order_filled(order.price, row): if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time) order.close_bt_order(current_time)
trade.open_order_id = None trade.open_order_id = None

View File

@ -114,8 +114,8 @@ class Hyperopt:
self.position_stacking = self.config.get('position_stacking', False) self.position_stacking = self.config.get('position_stacking', False)
if HyperoptTools.has_space(self.config, 'sell'): if HyperoptTools.has_space(self.config, 'sell'):
# Make sure use_sell_signal is enabled # Make sure use_exit_signal is enabled
self.config['use_sell_signal'] = True self.config['use_exit_signal'] = True
self.print_all = self.config.get('print_all', False) self.print_all = self.config.get('print_all', False)
self.hyperopt_table_header = 0 self.hyperopt_table_header = 0

View File

@ -460,10 +460,10 @@ def generate_strategy_stats(pairlist: List[str],
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False),
'use_custom_stoploss': config.get('use_custom_stoploss', False), 'use_custom_stoploss': config.get('use_custom_stoploss', False),
'minimal_roi': config['minimal_roi'], 'minimal_roi': config['minimal_roi'],
'use_sell_signal': config['use_sell_signal'], 'use_exit_signal': config['use_exit_signal'],
'sell_profit_only': config['sell_profit_only'], 'exit_profit_only': config['exit_profit_only'],
'sell_profit_offset': config['sell_profit_offset'], 'exit_profit_offset': config['exit_profit_offset'],
'ignore_roi_if_buy_signal': config['ignore_roi_if_buy_signal'], 'ignore_roi_if_entry_signal': config['ignore_roi_if_entry_signal'],
**daily_stats, **daily_stats,
**trade_stats **trade_stats
} }

View File

@ -3,6 +3,8 @@ from typing import List
from sqlalchemy import inspect, text from sqlalchemy import inspect, text
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -58,6 +60,8 @@ def migrate_trades_and_orders_table(
decl_base, inspector, engine, decl_base, inspector, engine,
trade_back_name: str, cols: List, trade_back_name: str, cols: List,
order_back_name: str, cols_order: List): order_back_name: str, cols_order: List):
base_currency = get_column_def(cols, 'base_currency', 'null')
stake_currency = get_column_def(cols, 'stake_currency', 'null')
fee_open = get_column_def(cols, 'fee_open', 'fee') fee_open = get_column_def(cols, 'fee_open', 'fee')
fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null') fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null') fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
@ -104,7 +108,8 @@ def migrate_trades_and_orders_table(
close_profit_abs = get_column_def( close_profit_abs = get_column_def(
cols, 'close_profit_abs', cols, 'close_profit_abs',
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}") f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}")
sell_order_status = get_column_def(cols, 'sell_order_status', 'null') exit_order_status = get_column_def(cols, 'exit_order_status',
get_column_def(cols, 'sell_order_status', 'null'))
amount_requested = get_column_def(cols, 'amount_requested', 'amount') amount_requested = get_column_def(cols, 'amount_requested', 'amount')
# Schema migration necessary # Schema migration necessary
@ -129,19 +134,20 @@ def migrate_trades_and_orders_table(
# Copy data back - following the correct schema # Copy data back - following the correct schema
with engine.begin() as connection: with engine.begin() as connection:
connection.execute(text(f"""insert into trades connection.execute(text(f"""insert into trades
(id, exchange, pair, is_open, (id, exchange, pair, base_currency, stake_currency, is_open,
fee_open, fee_open_cost, fee_open_currency, fee_open, fee_open_cost, fee_open_currency,
fee_close, fee_close_cost, fee_close_currency, open_rate, fee_close, fee_close_cost, fee_close_currency, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit, open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update, stoploss_order_id, stoploss_last_update,
max_rate, min_rate, exit_reason, sell_order_status, strategy, enter_tag, max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag,
timeframe, open_trade_value, close_profit_abs, timeframe, open_trade_value, close_profit_abs,
trading_mode, leverage, liquidation_price, is_short, trading_mode, leverage, liquidation_price, is_short,
interest_rate, funding_fees interest_rate, funding_fees
) )
select id, lower(exchange), pair, select id, lower(exchange), pair, {base_currency} base_currency,
{stake_currency} stake_currency,
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost, is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
{fee_open_currency} fee_open_currency, {fee_close} fee_close, {fee_open_currency} fee_open_currency, {fee_close} fee_close,
{fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency, {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
@ -152,8 +158,14 @@ def migrate_trades_and_orders_table(
{initial_stop_loss} initial_stop_loss, {initial_stop_loss} initial_stop_loss,
{initial_stop_loss_pct} initial_stop_loss_pct, {initial_stop_loss_pct} initial_stop_loss_pct,
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {exit_reason} exit_reason, {max_rate} max_rate, {min_rate} min_rate,
{sell_order_status} sell_order_status, case when {exit_reason} = 'sell_signal' then 'exit_signal'
when {exit_reason} = 'custom_sell' then 'custom_exit'
when {exit_reason} = 'force_sell' then 'force_exit'
when {exit_reason} = 'emergency_sell' then 'emergency_exit'
else {exit_reason}
end exit_reason,
{exit_order_status} exit_order_status,
{strategy} strategy, {enter_tag} enter_tag, {timeframe} timeframe, {strategy} strategy, {enter_tag} enter_tag, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
{trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price, {trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price,
@ -166,23 +178,6 @@ def migrate_trades_and_orders_table(
set_sequence_ids(engine, order_id, trade_id) set_sequence_ids(engine, order_id, trade_id)
def migrate_open_orders_to_trades(engine):
with engine.begin() as connection:
connection.execute(text("""
insert into orders (ft_trade_id, ft_pair, order_id, ft_order_side, ft_is_open)
select id ft_trade_id, pair ft_pair, open_order_id,
case when close_rate_requested is null then 'buy'
else 'sell' end ft_order_side, 1 ft_is_open
from trades
where open_order_id is not null
union all
select id ft_trade_id, pair ft_pair, stoploss_order_id order_id,
'stoploss' ft_order_side, 1 ft_is_open
from trades
where stoploss_order_id is not null
"""))
def drop_orders_table(engine, table_back_name: str): def drop_orders_table(engine, table_back_name: str):
# Drop and recreate orders table as backup # Drop and recreate orders table as backup
# This drops foreign keys, too. # This drops foreign keys, too.
@ -223,7 +218,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
""" """
inspector = inspect(engine) inspector = inspect(engine)
cols = inspector.get_columns('trades') cols_trades = inspector.get_columns('trades')
cols_orders = inspector.get_columns('orders') cols_orders = inspector.get_columns('orders')
tabs = get_table_names_for_table(inspector, 'trades') tabs = get_table_names_for_table(inspector, 'trades')
table_back_name = get_backup_name(tabs, 'trades_bak') table_back_name = get_backup_name(tabs, 'trades_bak')
@ -234,13 +229,17 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# Migrates both trades and orders table! # Migrates both trades and orders table!
# if ('orders' not in previous_tables # if ('orders' not in previous_tables
# or not has_column(cols_orders, 'leverage')): # or not has_column(cols_orders, 'leverage')):
if not has_column(cols, 'exit_reason'): if not has_column(cols_trades, 'base_currency'):
logger.info(f"Running database migration for trades - " logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}") f"backup: {table_back_name}, {order_table_bak_name}")
migrate_trades_and_orders_table( migrate_trades_and_orders_table(
decl_base, inspector, engine, table_back_name, cols, order_table_bak_name, cols_orders) decl_base, inspector, engine, table_back_name, cols_trades,
order_table_bak_name, cols_orders)
if 'orders' not in previous_tables and 'trades' in previous_tables: if 'orders' not in previous_tables and 'trades' in previous_tables:
logger.info('Moving open orders to Orders table.') raise OperationalException(
migrate_open_orders_to_trades(engine) "Your database seems to be very old. "
"Please update to freqtrade 2022.3 to migrate this database or "
"start with a fresh database.")
set_sqlite_to_wal(engine) set_sqlite_to_wal(engine)

View File

@ -279,6 +279,8 @@ class LocalTrade():
exchange: str = '' exchange: str = ''
pair: str = '' pair: str = ''
base_currency: str = ''
stake_currency: str = ''
is_open: bool = True is_open: bool = True
fee_open: float = 0.0 fee_open: float = 0.0
fee_open_cost: Optional[float] = None fee_open_cost: Optional[float] = None
@ -317,7 +319,7 @@ class LocalTrade():
# Lowest price reached # Lowest price reached
min_rate: float = 0.0 min_rate: float = 0.0
exit_reason: str = '' exit_reason: str = ''
sell_order_status: str = '' exit_order_status: str = ''
strategy: str = '' strategy: str = ''
enter_tag: Optional[str] = None enter_tag: Optional[str] = None
timeframe: Optional[int] = None timeframe: Optional[int] = None
@ -372,6 +374,12 @@ class LocalTrade():
@property @property
def enter_side(self) -> str: def enter_side(self) -> str:
""" DEPRECATED, please use entry_side instead"""
# TODO: Please remove me after 2022.5
return self.entry_side
@property
def entry_side(self) -> str:
if self.is_short: if self.is_short:
return "sell" return "sell"
else: else:
@ -391,6 +399,26 @@ class LocalTrade():
else: else:
return "long" return "long"
@property
def safe_base_currency(self) -> str:
"""
Compatibility layer for asset - which can be empty for old trades.
"""
try:
return self.base_currency or self.pair.split('/')[0]
except IndexError:
return ''
@property
def safe_quote_currency(self) -> str:
"""
Compatibility layer for asset - which can be empty for old trades.
"""
try:
return self.stake_currency or self.pair.split('/')[1].split(':')[0]
except IndexError:
return ''
def __init__(self, **kwargs): def __init__(self, **kwargs):
for key in kwargs: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
@ -412,11 +440,13 @@ class LocalTrade():
def to_json(self) -> Dict[str, Any]: def to_json(self) -> Dict[str, Any]:
filled_orders = self.select_filled_orders() filled_orders = self.select_filled_orders()
orders = [order.to_json(self.enter_side) for order in filled_orders] orders = [order.to_json(self.entry_side) for order in filled_orders]
return { return {
'trade_id': self.id, 'trade_id': self.id,
'pair': self.pair, 'pair': self.pair,
'base_currency': self.safe_base_currency,
'quote_currency': self.safe_quote_currency,
'is_open': self.is_open, 'is_open': self.is_open,
'exchange': self.exchange, 'exchange': self.exchange,
'amount': round(self.amount, 8), 'amount': round(self.amount, 8),
@ -461,7 +491,7 @@ class LocalTrade():
'sell_reason': self.exit_reason, # Deprecated 'sell_reason': self.exit_reason, # Deprecated
'exit_reason': self.exit_reason, 'exit_reason': self.exit_reason,
'sell_order_status': self.sell_order_status, 'exit_order_status': self.exit_order_status,
'stop_loss_abs': self.stop_loss, 'stop_loss_abs': self.stop_loss,
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
@ -601,7 +631,7 @@ class LocalTrade():
logger.info(f'Updating trade (id={self.id}) ...') logger.info(f'Updating trade (id={self.id}) ...')
if order.ft_order_side == self.enter_side: if order.ft_order_side == self.entry_side:
# Update open rate and actual amount # Update open rate and actual amount
self.open_rate = order.safe_price self.open_rate = order.safe_price
self.amount = order.safe_amount_after_fee self.amount = order.safe_amount_after_fee
@ -637,7 +667,7 @@ class LocalTrade():
self.close_profit = self.calc_profit_ratio() self.close_profit = self.calc_profit_ratio()
self.close_profit_abs = self.calc_profit() self.close_profit_abs = self.calc_profit()
self.is_open = False self.is_open = False
self.sell_order_status = 'closed' self.exit_order_status = 'closed'
self.open_order_id = None self.open_order_id = None
if show_msg: if show_msg:
logger.info( logger.info(
@ -650,7 +680,7 @@ class LocalTrade():
""" """
Update Fee parameters. Only acts once per side Update Fee parameters. Only acts once per side
""" """
if self.enter_side == side and self.fee_open_currency is None: if self.entry_side == side and self.fee_open_currency is None:
self.fee_open_cost = fee_cost self.fee_open_cost = fee_cost
self.fee_open_currency = fee_currency self.fee_open_currency = fee_currency
if fee_rate is not None: if fee_rate is not None:
@ -667,7 +697,7 @@ class LocalTrade():
""" """
Verify if this side (buy / sell) has already been updated Verify if this side (buy / sell) has already been updated
""" """
if self.enter_side == side: if self.entry_side == side:
return self.fee_open_currency is not None return self.fee_open_currency is not None
elif self.exit_side == side: elif self.exit_side == side:
return self.fee_close_currency is not None return self.fee_close_currency is not None
@ -840,7 +870,7 @@ class LocalTrade():
def recalc_trade_from_orders(self): def recalc_trade_from_orders(self):
# We need at least 2 entry orders for averaging amounts and rates. # We need at least 2 entry orders for averaging amounts and rates.
# TODO: this condition could probably be removed # TODO: this condition could probably be removed
if len(self.select_filled_orders(self.enter_side)) < 2: if len(self.select_filled_orders(self.entry_side)) < 2:
self.stake_amount = self.amount * self.open_rate / self.leverage self.stake_amount = self.amount * self.open_rate / self.leverage
# Just in case, still recalc open trade value # Just in case, still recalc open trade value
@ -851,7 +881,7 @@ class LocalTrade():
total_stake = 0.0 total_stake = 0.0
for o in self.orders: for o in self.orders:
if (o.ft_is_open or if (o.ft_is_open or
(o.ft_order_side != self.enter_side) or (o.ft_order_side != self.entry_side) or
(o.status not in NON_OPEN_EXCHANGE_STATES)): (o.status not in NON_OPEN_EXCHANGE_STATES)):
continue continue
@ -919,7 +949,7 @@ class LocalTrade():
:return: int count of entry orders that have been filled for this trade. :return: int count of entry orders that have been filled for this trade.
""" """
return len(self.select_filled_orders(self.enter_side)) return len(self.select_filled_orders(self.entry_side))
@property @property
def nr_of_successful_exits(self) -> int: def nr_of_successful_exits(self) -> int:
@ -1045,6 +1075,8 @@ class Trade(_DECL_BASE, LocalTrade):
exchange = Column(String(25), nullable=False) exchange = Column(String(25), nullable=False)
pair = Column(String(25), nullable=False, index=True) pair = Column(String(25), nullable=False, index=True)
base_currency = Column(String(25), nullable=True)
stake_currency = Column(String(25), nullable=True)
is_open = Column(Boolean, nullable=False, default=True, index=True) is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0) fee_open = Column(Float, nullable=False, default=0.0)
fee_open_cost = Column(Float, nullable=True) fee_open_cost = Column(Float, nullable=True)
@ -1083,7 +1115,7 @@ class Trade(_DECL_BASE, LocalTrade):
# Lowest price reached # Lowest price reached
min_rate = Column(Float, nullable=True) min_rate = Column(Float, nullable=True)
exit_reason = Column(String(100), nullable=True) exit_reason = Column(String(100), nullable=True)
sell_order_status = Column(String(100), nullable=True) exit_order_status = Column(String(100), nullable=True)
strategy = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True)
enter_tag = Column(String(100), nullable=True) enter_tag = Column(String(100), nullable=True)
timeframe = Column(Integer, nullable=True) timeframe = Column(Integer, nullable=True)
@ -1291,7 +1323,7 @@ class Trade(_DECL_BASE, LocalTrade):
@staticmethod @staticmethod
def get_exit_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]: def get_exit_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
""" """
Returns List of dicts containing all Trades, based on sell reason performance Returns List of dicts containing all Trades, based on exit reason performance
Can either be average for all pairs or a specific pair provided Can either be average for all pairs or a specific pair provided
NOTE: Not supported in Backtesting. NOTE: Not supported in Backtesting.
""" """

View File

@ -85,10 +85,10 @@ class StrategyResolver(IResolver):
("protections", None), ("protections", None),
("startup_candle_count", None), ("startup_candle_count", None),
("unfilledtimeout", None), ("unfilledtimeout", None),
("use_sell_signal", True), ("use_exit_signal", True),
("sell_profit_only", False), ("exit_profit_only", False),
("ignore_roi_if_buy_signal", False), ("ignore_roi_if_entry_signal", False),
("sell_profit_offset", 0.0), ("exit_profit_offset", 0.0),
("disable_dataframe_checks", False), ("disable_dataframe_checks", False),
("ignore_buying_expired_candle_after", 0), ("ignore_buying_expired_candle_after", 0),
("position_adjustment_enable", False), ("position_adjustment_enable", False),
@ -173,6 +173,12 @@ class StrategyResolver(IResolver):
def validate_strategy(strategy: IStrategy) -> IStrategy: def validate_strategy(strategy: IStrategy) -> IStrategy:
if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
# Require new method # Require new method
warn_deprecated_setting(strategy, 'sell_profit_only', 'exit_profit_only', True)
warn_deprecated_setting(strategy, 'sell_profit_offset', 'exit_profit_offset', True)
warn_deprecated_setting(strategy, 'use_sell_signal', 'use_exit_signal', True)
warn_deprecated_setting(strategy, 'ignore_roi_if_buy_signal',
'ignore_roi_if_entry_signal', True)
if not check_override(strategy, IStrategy, 'populate_entry_trend'): if not check_override(strategy, IStrategy, 'populate_entry_trend'):
raise OperationalException("`populate_entry_trend` must be implemented.") raise OperationalException("`populate_entry_trend` must be implemented.")
if not check_override(strategy, IStrategy, 'populate_exit_trend'): if not check_override(strategy, IStrategy, 'populate_exit_trend'):
@ -187,9 +193,16 @@ class StrategyResolver(IResolver):
if check_override(strategy, IStrategy, 'custom_sell'): if check_override(strategy, IStrategy, 'custom_sell'):
raise OperationalException( raise OperationalException(
"Please migrate your implementation of `custom_sell` to `custom_exit`.") "Please migrate your implementation of `custom_sell` to `custom_exit`.")
else: else:
# TODO: Implementing one of the following methods should show a deprecation warning # TODO: Implementing one of the following methods should show a deprecation warning
# buy_trend and sell_trend, custom_sell # buy_trend and sell_trend, custom_sell
warn_deprecated_setting(strategy, 'sell_profit_only', 'exit_profit_only')
warn_deprecated_setting(strategy, 'sell_profit_offset', 'exit_profit_offset')
warn_deprecated_setting(strategy, 'use_sell_signal', 'use_exit_signal')
warn_deprecated_setting(strategy, 'ignore_roi_if_buy_signal',
'ignore_roi_if_entry_signal')
if ( if (
not check_override(strategy, IStrategy, 'populate_buy_trend') not check_override(strategy, IStrategy, 'populate_buy_trend')
and not check_override(strategy, IStrategy, 'populate_entry_trend') and not check_override(strategy, IStrategy, 'populate_entry_trend')
@ -262,6 +275,15 @@ class StrategyResolver(IResolver):
) )
def warn_deprecated_setting(strategy: IStrategy, old: str, new: str, error=False):
if hasattr(strategy, old):
errormsg = f"DEPRECATED: Using '{old}' moved to '{new}'."
if error:
raise OperationalException(errormsg)
logger.warning(errormsg)
setattr(strategy, new, getattr(strategy, f'{old}'))
def check_override(object, parentclass, attribute): def check_override(object, parentclass, attribute):
""" """
Checks if a object overrides the parent class attribute. Checks if a object overrides the parent class attribute.

View File

@ -140,9 +140,9 @@ class UnfilledTimeout(BaseModel):
class OrderTypes(BaseModel): class OrderTypes(BaseModel):
entry: OrderTypeValues entry: OrderTypeValues
exit: OrderTypeValues exit: OrderTypeValues
emergencyexit: Optional[OrderTypeValues] emergency_exit: Optional[OrderTypeValues]
forceexit: Optional[OrderTypeValues] force_exit: Optional[OrderTypeValues]
forceentry: Optional[OrderTypeValues] force_entry: Optional[OrderTypeValues]
stoploss: OrderTypeValues stoploss: OrderTypeValues
stoploss_on_exchange: bool stoploss_on_exchange: bool
stoploss_on_exchange_interval: Optional[int] stoploss_on_exchange_interval: Optional[int]
@ -174,7 +174,7 @@ class ShowConfig(BaseModel):
timeframe_min: int timeframe_min: int
exchange: str exchange: str
strategy: Optional[str] strategy: Optional[str]
forcebuy_enabled: bool force_entry_enable: bool
exit_pricing: Dict[str, Any] exit_pricing: Dict[str, Any]
entry_pricing: Dict[str, Any] entry_pricing: Dict[str, Any]
bot_name: str bot_name: str
@ -203,6 +203,8 @@ class OrderSchema(BaseModel):
class TradeSchema(BaseModel): class TradeSchema(BaseModel):
trade_id: int trade_id: int
pair: str pair: str
base_currency: str
quote_currency: str
is_open: bool is_open: bool
is_short: bool is_short: bool
exchange: str exchange: str
@ -237,7 +239,7 @@ class TradeSchema(BaseModel):
profit_fiat: Optional[float] profit_fiat: Optional[float]
sell_reason: Optional[str] # Deprecated sell_reason: Optional[str] # Deprecated
exit_reason: Optional[str] exit_reason: Optional[str]
sell_order_status: Optional[str] exit_order_status: Optional[str]
stop_loss_abs: Optional[float] stop_loss_abs: Optional[float]
stop_loss_ratio: Optional[float] stop_loss_ratio: Optional[float]
stop_loss_pct: Optional[float] stop_loss_pct: Optional[float]

View File

@ -135,13 +135,13 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
return resp return resp
# /forcebuy is deprecated with short addition. use ForceEntry instead # /forcebuy is deprecated with short addition. use /forceentry instead
@router.post('/forceenter', response_model=ForceEnterResponse, tags=['trading']) @router.post('/forceenter', response_model=ForceEnterResponse, tags=['trading'])
@router.post('/forcebuy', response_model=ForceEnterResponse, tags=['trading']) @router.post('/forcebuy', response_model=ForceEnterResponse, tags=['trading'])
def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None ordertype = payload.ordertype.value if payload.ordertype else None
stake_amount = payload.stakeamount if payload.stakeamount else None stake_amount = payload.stakeamount if payload.stakeamount else None
entry_tag = payload.entry_tag if payload.entry_tag else 'forceentry' entry_tag = payload.entry_tag if payload.entry_tag else 'force_entry'
trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side, trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
order_type=ordertype, stake_amount=stake_amount, order_type=ordertype, stake_amount=stake_amount,
@ -154,11 +154,12 @@ def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
{"status": f"Error entering {payload.side} trade for pair {payload.pair}."}) {"status": f"Error entering {payload.side} trade for pair {payload.pair}."})
# /forcesell is deprecated with short addition. use /forceexit instead
@router.post('/forceexit', response_model=ResultMsg, tags=['trading']) @router.post('/forceexit', response_model=ResultMsg, tags=['trading'])
@router.post('/forcesell', response_model=ResultMsg, tags=['trading']) @router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
def forcesell(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)): def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None ordertype = payload.ordertype.value if payload.ordertype else None
return rpc._rpc_forceexit(payload.tradeid, ordertype) return rpc._rpc_force_exit(payload.tradeid, ordertype)
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) @router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])

View File

@ -86,7 +86,7 @@ class CryptoToFiatConverter:
return None return None
else: else:
return None return None
found = [x for x in self._coinlistings if x['symbol'] == crypto_symbol] found = [x for x in self._coinlistings if x['symbol'].lower() == crypto_symbol]
if crypto_symbol in coingecko_mapping.keys(): if crypto_symbol in coingecko_mapping.keys():
found = [x for x in self._coinlistings if x['id'] == coingecko_mapping[crypto_symbol]] found = [x for x in self._coinlistings if x['id'] == coingecko_mapping[crypto_symbol]]

View File

@ -136,7 +136,7 @@ class RPC:
) if 'timeframe' in config else 0, ) if 'timeframe' in config else 0,
'exchange': config['exchange']['name'], 'exchange': config['exchange']['name'],
'strategy': config['strategy'], 'strategy': config['strategy'],
'forcebuy_enabled': config.get('forcebuy_enable', False), 'force_entry_enable': config.get('force_entry_enable', False),
'exit_pricing': config.get('exit_pricing', {}), 'exit_pricing': config.get('exit_pricing', {}),
'entry_pricing': config.get('entry_pricing', {}), 'entry_pricing': config.get('entry_pricing', {}),
'state': str(botstate), 'state': str(botstate),
@ -197,7 +197,6 @@ class RPC:
trade_dict = trade.to_json() trade_dict = trade.to_json()
trade_dict.update(dict( trade_dict.update(dict(
base_currency=self._freqtrade.config['stake_currency'],
close_profit=trade.close_profit if trade.close_profit is not None else None, close_profit=trade.close_profit if trade.close_profit is not None else None,
current_rate=current_rate, current_rate=current_rate,
current_profit=current_profit, # Deprecated current_profit=current_profit, # Deprecated
@ -223,6 +222,7 @@ class RPC:
def _rpc_status_table(self, stake_currency: str, def _rpc_status_table(self, stake_currency: str,
fiat_display_currency: str) -> Tuple[List, List, float]: fiat_display_currency: str) -> Tuple[List, List, float]:
trades: List[Trade] = Trade.get_open_trades() trades: List[Trade] = Trade.get_open_trades()
nonspot = self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT
if not trades: if not trades:
raise RPCException('no active trade') raise RPCException('no active trade')
else: else:
@ -237,7 +237,7 @@ class RPC:
current_rate = NAN current_rate = NAN
trade_profit = trade.calc_profit(current_rate) trade_profit = trade.calc_profit(current_rate)
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}' profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
direction_str = 'S' if trade.is_short else 'L' direction_str = ('S' if trade.is_short else 'L') if nonspot else ''
if self._fiat_converter: if self._fiat_converter:
fiat_profit = self._fiat_converter.convert_amount( fiat_profit = self._fiat_converter.convert_amount(
trade_profit, trade_profit,
@ -267,7 +267,11 @@ class RPC:
if self._fiat_converter: if self._fiat_converter:
profitcol += " (" + fiat_display_currency + ")" profitcol += " (" + fiat_display_currency + ")"
columns = ['ID L/S', 'Pair', 'Since', profitcol] columns = [
'ID L/S' if nonspot else 'ID',
'Pair',
'Since',
profitcol]
if self._config.get('position_adjustment_enable', False): if self._config.get('position_adjustment_enable', False):
columns.append('# Entries') columns.append('# Entries')
return trades_list, columns, fiat_profit_sum return trades_list, columns, fiat_profit_sum
@ -684,32 +688,32 @@ class RPC:
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'} return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
def _rpc_forceexit(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]: def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
""" """
Handler for forcesell <id>. Handler for forceexit <id>.
Sells the given trade at current price Sells the given trade at current price
""" """
def _exec_forcesell(trade: Trade) -> None: def _exec_force_exit(trade: Trade) -> None:
# Check if there is there is an open order # Check if there is there is an open order
fully_canceled = False fully_canceled = False
if trade.open_order_id: if trade.open_order_id:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
if order['side'] == trade.enter_side: if order['side'] == trade.entry_side:
fully_canceled = self._freqtrade.handle_cancel_enter( fully_canceled = self._freqtrade.handle_cancel_enter(
trade, order, CANCEL_REASON['FORCE_SELL']) trade, order, CANCEL_REASON['FORCE_EXIT'])
if order['side'] == trade.exit_side: if order['side'] == trade.exit_side:
# Cancel order - so it is placed anew with a fresh price. # Cancel order - so it is placed anew with a fresh price.
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL']) self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT'])
if not fully_canceled: if not fully_canceled:
# Get current rate and execute sell # Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate( current_rate = self._freqtrade.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=True) trade.pair, side='exit', is_short=trade.is_short, refresh=True)
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_SELL) exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
order_type = ordertype or self._freqtrade.strategy.order_types.get( order_type = ordertype or self._freqtrade.strategy.order_types.get(
"forceexit", self._freqtrade.strategy.order_types["exit"]) "force_exit", self._freqtrade.strategy.order_types["exit"])
self._freqtrade.execute_trade_exit( self._freqtrade.execute_trade_exit(
trade, current_rate, exit_check, ordertype=order_type) trade, current_rate, exit_check, ordertype=order_type)
@ -722,7 +726,7 @@ class RPC:
if trade_id == 'all': if trade_id == 'all':
# Execute sell for all open orders # Execute sell for all open orders
for trade in Trade.get_open_trades(): for trade in Trade.get_open_trades():
_exec_forcesell(trade) _exec_force_exit(trade)
Trade.commit() Trade.commit()
self._freqtrade.wallets.update() self._freqtrade.wallets.update()
return {'result': 'Created sell orders for all open trades.'} return {'result': 'Created sell orders for all open trades.'}
@ -732,10 +736,10 @@ class RPC:
trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ] trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
).first() ).first()
if not trade: if not trade:
logger.warning('forceexit: Invalid argument received') logger.warning('force_exit: Invalid argument received')
raise RPCException('invalid argument') raise RPCException('invalid argument')
_exec_forcesell(trade) _exec_force_exit(trade)
Trade.commit() Trade.commit()
self._freqtrade.wallets.update() self._freqtrade.wallets.update()
return {'result': f'Created sell order for trade {trade_id}.'} return {'result': f'Created sell order for trade {trade_id}.'}
@ -744,14 +748,14 @@ class RPC:
order_type: Optional[str] = None, order_type: Optional[str] = None,
order_side: SignalDirection = SignalDirection.LONG, order_side: SignalDirection = SignalDirection.LONG,
stake_amount: Optional[float] = None, stake_amount: Optional[float] = None,
enter_tag: Optional[str] = 'forceentry') -> Optional[Trade]: enter_tag: Optional[str] = 'force_entry') -> Optional[Trade]:
""" """
Handler for forcebuy <asset> <price> Handler for forcebuy <asset> <price>
Buys a pair trade at the given or current price Buys a pair trade at the given or current price
""" """
if not self._freqtrade.config.get('forcebuy_enable', False): if not self._freqtrade.config.get('force_entry_enable', False):
raise RPCException('Forceentry not enabled.') raise RPCException('Force_entry not enabled.')
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running') raise RPCException('trader is not running')
@ -781,7 +785,7 @@ class RPC:
# execute buy # execute buy
if not order_type: if not order_type:
order_type = self._freqtrade.strategy.order_types.get( order_type = self._freqtrade.strategy.order_types.get(
'forceentry', self._freqtrade.strategy.order_types['entry']) 'force_entry', self._freqtrade.strategy.order_types['entry'])
if self._freqtrade.execute_entry(pair, stake_amount, price, if self._freqtrade.execute_entry(pair, stake_amount, price,
ordertype=order_type, trade=trade, ordertype=order_type, trade=trade,
is_short=is_short, is_short=is_short,
@ -791,7 +795,7 @@ class RPC:
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade return trade
else: else:
return None raise RPCException(f'Failed to enter position for {pair}.')
def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]: def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
""" """
@ -850,7 +854,7 @@ class RPC:
def _rpc_exit_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]: def _rpc_exit_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
""" """
Handler for sell reason performance. Handler for exit reason performance.
Shows a performance statistic from finished trades Shows a performance statistic from finished trades
""" """
return Trade.get_exit_reason_performance(pair) return Trade.get_exit_reason_performance(pair)

View File

@ -103,7 +103,6 @@ class Telegram(RPCHandler):
['/count', '/start', '/stop', '/help'] ['/count', '/start', '/stop', '/help']
] ]
# do not allow commands with mandatory arguments and critical cmds # do not allow commands with mandatory arguments and critical cmds
# like /forcesell and /forcebuy
# TODO: DRY! - its not good to list all valid cmds here. But otherwise # TODO: DRY! - its not good to list all valid cmds here. But otherwise
# this needs refactoring of the whole telegram module (same # this needs refactoring of the whole telegram module (same
# problem in _help()). # problem in _help()).
@ -116,6 +115,7 @@ class Telegram(RPCHandler):
r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
r'/forcebuy$', r'/forcelong$', r'/forceshort$', r'/forcebuy$', r'/forcelong$', r'/forceshort$',
r'/forcesell$', r'/forceexit$',
r'/edge$', r'/health$', r'/help$', r'/version$'] r'/edge$', r'/health$', r'/help$', r'/version$']
# Create keys for generation # Create keys for generation
valid_keys_print = [k.replace('$', '') for k in valid_keys] valid_keys_print = [k.replace('$', '') for k in valid_keys]
@ -153,11 +153,11 @@ class Telegram(RPCHandler):
CommandHandler('balance', self._balance), CommandHandler('balance', self._balance),
CommandHandler('start', self._start), CommandHandler('start', self._start),
CommandHandler('stop', self._stop), CommandHandler('stop', self._stop),
CommandHandler(['forcesell', 'forceexit'], self._forceexit), CommandHandler(['forcesell', 'forceexit', 'fx'], self._force_exit),
CommandHandler(['forcebuy', 'forcelong'], partial( CommandHandler(['forcebuy', 'forcelong'], partial(
self._forceenter, order_side=SignalDirection.LONG)), self._force_enter, order_side=SignalDirection.LONG)),
CommandHandler('forceshort', partial( CommandHandler('forceshort', partial(
self._forceenter, order_side=SignalDirection.SHORT)), self._force_enter, order_side=SignalDirection.SHORT)),
CommandHandler('trades', self._trades), CommandHandler('trades', self._trades),
CommandHandler('delete', self._delete_trade), CommandHandler('delete', self._delete_trade),
CommandHandler('performance', self._performance), CommandHandler('performance', self._performance),
@ -197,7 +197,8 @@ class Telegram(RPCHandler):
pattern='update_exit_reason_performance'), pattern='update_exit_reason_performance'),
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
CallbackQueryHandler(self._count, pattern='update_count'), CallbackQueryHandler(self._count, pattern='update_count'),
CallbackQueryHandler(self._forceenter_inline), CallbackQueryHandler(self._force_exit_inline, pattern=r"force_exit__\S+"),
CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"),
] ]
for handle in handles: for handle in handles:
self._updater.dispatcher.add_handler(handle) self._updater.dispatcher.add_handler(handle)
@ -224,21 +225,20 @@ class Telegram(RPCHandler):
# This can take up to `timeout` from the call to `start_polling`. # This can take up to `timeout` from the call to `start_polling`.
self._updater.stop() self._updater.stop()
def _format_buy_msg(self, msg: Dict[str, Any]) -> str: def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
if self._rpc._fiat_converter: if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
else: else:
msg['stake_amount_fiat'] = 0 msg['stake_amount_fiat'] = 0
is_fill = msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL] is_fill = msg['type'] in [RPCMessageType.ENTRY_FILL]
emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}' emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}'
enter_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['type'] entry_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
in [RPCMessageType.BUY_FILL, RPCMessageType.BUY]
else {'enter': 'Short', 'entered': 'Shorted'}) else {'enter': 'Short', 'entered': 'Shorted'})
message = ( message = (
f"{emoji} *{msg['exchange']}:*" f"{emoji} *{msg['exchange']}:*"
f" {enter_side['entered'] if is_fill else enter_side['enter']} {msg['pair']}" f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
f" (#{msg['trade_id']})\n" f" (#{msg['trade_id']})\n"
) )
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else "" message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else ""
@ -246,9 +246,9 @@ class Telegram(RPCHandler):
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0: if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
message += f"*Leverage:* `{msg['leverage']}`\n" message += f"*Leverage:* `{msg['leverage']}`\n"
if msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]: if msg['type'] in [RPCMessageType.ENTRY_FILL]:
message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n" message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
elif msg['type'] in [RPCMessageType.BUY, RPCMessageType.SHORT]: elif msg['type'] in [RPCMessageType.ENTRY]:
message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\ message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\
f"*Current Rate:* `{msg['current_rate']:.8f}`\n" f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
@ -260,7 +260,7 @@ class Telegram(RPCHandler):
message += ")`" message += ")`"
return message return message
def _format_sell_msg(self, msg: Dict[str, Any]) -> str: def _format_exit_msg(self, msg: Dict[str, Any]) -> str:
msg['amount'] = round(msg['amount'], 8) msg['amount'] = round(msg['amount'], 8)
msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2)
msg['duration'] = msg['close_date'].replace( msg['duration'] = msg['close_date'].replace(
@ -274,7 +274,7 @@ class Telegram(RPCHandler):
else "") else "")
# Check if all sell properties are available. # Check if all sell properties are available.
# This might not be the case if the message origin is triggered by /forcesell # This might not be the case if the message origin is triggered by /forceexit
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
and self._rpc._fiat_converter): and self._rpc._fiat_converter):
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
@ -284,7 +284,7 @@ class Telegram(RPCHandler):
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})") f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})")
else: else:
msg['profit_extra'] = '' msg['profit_extra'] = ''
is_fill = msg['type'] == RPCMessageType.SELL_FILL is_fill = msg['type'] == RPCMessageType.EXIT_FILL
message = ( message = (
f"{msg['emoji']} *{msg['exchange']}:* " f"{msg['emoji']} *{msg['exchange']}:* "
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
@ -298,27 +298,24 @@ class Telegram(RPCHandler):
f"*Amount:* `{msg['amount']:.8f}`\n" f"*Amount:* `{msg['amount']:.8f}`\n"
f"*Open Rate:* `{msg['open_rate']:.8f}`\n" f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
) )
if msg['type'] == RPCMessageType.SELL: if msg['type'] == RPCMessageType.EXIT:
message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n" message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
f"*Close Rate:* `{msg['limit']:.8f}`") f"*Close Rate:* `{msg['limit']:.8f}`")
elif msg['type'] == RPCMessageType.SELL_FILL: elif msg['type'] == RPCMessageType.EXIT_FILL:
message += f"*Close Rate:* `{msg['close_rate']:.8f}`" message += f"*Close Rate:* `{msg['close_rate']:.8f}`"
return message return message
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL, RPCMessageType.SHORT, if msg_type in [RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]:
RPCMessageType.SHORT_FILL]: message = self._format_entry_msg(msg)
message = self._format_buy_msg(msg)
elif msg_type in [RPCMessageType.SELL, RPCMessageType.SELL_FILL]: elif msg_type in [RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]:
message = self._format_sell_msg(msg) message = self._format_exit_msg(msg)
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SHORT_CANCEL, elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
RPCMessageType.SELL_CANCEL): msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.BUY_CANCEL,
RPCMessageType.SHORT_CANCEL] else 'exit'
message = ("\N{WARNING SIGN} *{exchange}:* " message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling {message_side} Order for {pair} (#{trade_id}). " "Cancelling {message_side} Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg)) "Reason: {reason}.".format(**msg))
@ -355,7 +352,7 @@ class Telegram(RPCHandler):
msg_type = msg['type'] msg_type = msg['type']
noti = '' noti = ''
if msg_type == RPCMessageType.SELL: if msg_type == RPCMessageType.EXIT:
sell_noti = self._config['telegram'] \ sell_noti = self._config['telegram'] \
.get('notification_settings', {}).get(str(msg_type), {}) .get('notification_settings', {}).get(str(msg_type), {})
# For backward compatibility sell still can be string # For backward compatibility sell still can be string
@ -422,7 +419,8 @@ class Telegram(RPCHandler):
if prev_avg_price: if prev_avg_price:
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"]) dur_entry = cur_entry_datetime - arrow.get(
filled_orders[x - 1]["order_filled_date"])
days = dur_entry.days days = dur_entry.days
hours, remainder = divmod(dur_entry.seconds, 3600) hours, remainder = divmod(dur_entry.seconds, 3600)
minutes, seconds = divmod(remainder, 60) minutes, seconds = divmod(remainder, 60)
@ -506,13 +504,13 @@ class Telegram(RPCHandler):
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
"`({stoploss_current_dist_ratio:.2%})`") "`({stoploss_current_dist_ratio:.2%})`")
if r['open_order']: if r['open_order']:
if r['sell_order_status']: if r['exit_order_status']:
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`") lines.append("*Open Order:* `{open_order}` - `{exit_order_status}`")
else: else:
lines.append("*Open Order:* `{open_order}`") lines.append("*Open Order:* `{open_order}`")
lines_detail = self._prepare_entry_details( lines_detail = self._prepare_entry_details(
r['orders'], r['base_currency'], r['is_open']) r['orders'], r['quote_currency'], r['is_open'])
lines.extend(lines_detail if lines_detail else "") lines.extend(lines_detail if lines_detail else "")
# Filter empty lines using list-comprehension # Filter empty lines using list-comprehension
@ -768,9 +766,9 @@ class Telegram(RPCHandler):
'stop_loss': 'Stoploss', 'stop_loss': 'Stoploss',
'trailing_stop_loss': 'Trail. Stop', 'trailing_stop_loss': 'Trail. Stop',
'stoploss_on_exchange': 'Stoploss', 'stoploss_on_exchange': 'Stoploss',
'sell_signal': 'Sell Signal', 'exit_signal': 'Exit Signal',
'force_sell': 'Forcesell', 'force_exit': 'Force Exit',
'emergency_sell': 'Emergency Sell', 'emergency_exit': 'Emergency Exit',
} }
exit_reasons_tabulate = [ exit_reasons_tabulate = [
[ [
@ -930,34 +928,69 @@ class Telegram(RPCHandler):
self._send_msg('Status: `{status}`'.format(**msg)) self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only @authorized_only
def _forceexit(self, update: Update, context: CallbackContext) -> None: def _force_exit(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /forcesell <id>. Handler for /forceexit <id>.
Sells the given trade at current price Sells the given trade at current price
:param bot: telegram bot :param bot: telegram bot
:param update: message update :param update: message update
:return: None :return: None
""" """
trade_id = context.args[0] if context.args and len(context.args) > 0 else None if context.args:
if not trade_id: trade_id = context.args[0]
self._send_msg("You must specify a trade-id or 'all'.") self._force_exit_action(trade_id)
return else:
fiat_currency = self._config.get('fiat_display_currency', '')
try: try:
msg = self._rpc._rpc_forceexit(trade_id) statlist, head, fiat_profit_sum = self._rpc._rpc_status_table(
self._send_msg('Forceexit Result: `{result}`'.format(**msg)) self._config['stake_currency'], fiat_currency)
except RPCException:
self._send_msg(msg='No open trade found.')
return
trades = []
for trade in statlist:
trades.append((trade[0], f"{trade[0]} {trade[1]} {trade[2]} {trade[3]}"))
trade_buttons = [
InlineKeyboardButton(text=trade[1], callback_data=f"force_exit__{trade[0]}")
for trade in trades]
buttons_aligned = self._layout_inline_keyboard_onecol(trade_buttons)
buttons_aligned.append([InlineKeyboardButton(
text='Cancel', callback_data='force_exit__cancel')])
self._send_msg(msg="Which trade?", keyboard=buttons_aligned)
def _force_exit_action(self, trade_id):
if trade_id != 'cancel':
try:
self._rpc._rpc_force_exit(trade_id)
except RPCException as e: except RPCException as e:
self._send_msg(str(e)) self._send_msg(str(e))
def _forceenter_action(self, pair, price: Optional[float], order_side: SignalDirection): def _force_exit_inline(self, update: Update, _: CallbackContext) -> None:
if update.callback_query:
query = update.callback_query
if query.data and '__' in query.data:
# Input data is "force_exit__<tradid|cancel>"
trade_id = query.data.split("__")[1].split(' ')[0]
if trade_id == 'cancel':
query.answer()
query.edit_message_text(text="Force exit canceled.")
return
trade: Trade = Trade.get_trades(trade_filter=Trade.id == trade_id).first()
query.answer()
query.edit_message_text(text=f"Manually exiting Trade #{trade_id}, {trade.pair}")
self._force_exit_action(trade_id)
def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
if pair != 'cancel': if pair != 'cancel':
try: try:
self._rpc._rpc_force_entry(pair, price, order_side=order_side) self._rpc._rpc_force_entry(pair, price, order_side=order_side)
except RPCException as e: except RPCException as e:
self._send_msg(str(e)) self._send_msg(str(e))
def _forceenter_inline(self, update: Update, _: CallbackContext) -> None: def _force_enter_inline(self, update: Update, _: CallbackContext) -> None:
if update.callback_query: if update.callback_query:
query = update.callback_query query = update.callback_query
if query.data and '_||_' in query.data: if query.data and '_||_' in query.data:
@ -965,15 +998,20 @@ class Telegram(RPCHandler):
order_side = SignalDirection(side) order_side = SignalDirection(side)
query.answer() query.answer()
query.edit_message_text(text=f"Manually entering {order_side} for {pair}") query.edit_message_text(text=f"Manually entering {order_side} for {pair}")
self._forceenter_action(pair, None, order_side) self._force_enter_action(pair, None, order_side)
@staticmethod @staticmethod
def _layout_inline_keyboard(buttons: List[InlineKeyboardButton], def _layout_inline_keyboard(
cols=3) -> List[List[InlineKeyboardButton]]: buttons: List[InlineKeyboardButton], cols=3) -> List[List[InlineKeyboardButton]]:
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
@staticmethod
def _layout_inline_keyboard_onecol(
buttons: List[InlineKeyboardButton], cols=1) -> List[List[InlineKeyboardButton]]:
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
@authorized_only @authorized_only
def _forceenter( def _force_enter(
self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None: self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None:
""" """
Handler for /forcelong <asset> <price> and `/forceshort <asset> <price> Handler for /forcelong <asset> <price> and `/forceshort <asset> <price>
@ -985,7 +1023,7 @@ class Telegram(RPCHandler):
if context.args: if context.args:
pair = context.args[0] pair = context.args[0]
price = float(context.args[1]) if len(context.args) > 1 else None price = float(context.args[1]) if len(context.args) > 1 else None
self._forceenter_action(pair, price, order_side) self._force_enter_action(pair, price, order_side)
else: else:
whitelist = self._rpc._rpc_whitelist()['whitelist'] whitelist = self._rpc._rpc_whitelist()['whitelist']
pair_buttons = [ pair_buttons = [
@ -1363,12 +1401,12 @@ class Telegram(RPCHandler):
:param update: message update :param update: message update
:return: None :return: None
""" """
forceenter_text = ("*/forcelong <pair> [<rate>]:* `Instantly buys the given pair. " force_enter_text = ("*/forcelong <pair> [<rate>]:* `Instantly buys the given pair. "
"Optionally takes a rate at which to buy " "Optionally takes a rate at which to buy "
"(only applies to limit orders).` \n" "(only applies to limit orders).` \n"
) )
if self._rpc._freqtrade.trading_mode != TradingMode.SPOT: if self._rpc._freqtrade.trading_mode != TradingMode.SPOT:
forceenter_text += ("*/forceshort <pair> [<rate>]:* `Instantly shorts the given pair. " force_enter_text += ("*/forceshort <pair> [<rate>]:* `Instantly shorts the given pair. "
"Optionally takes a rate at which to sell " "Optionally takes a rate at which to sell "
"(only applies to limit orders).` \n") "(only applies to limit orders).` \n")
message = ( message = (
@ -1379,7 +1417,8 @@ class Telegram(RPCHandler):
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, " "*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
"regardless of profit`\n" "regardless of profit`\n"
f"{forceenter_text if self._config.get('forcebuy_enable', False) else ''}" "*/fe <trade_id>|all:* `Alias to /forceexit`"
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n" "*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/whitelist:* `Show current whitelist` \n" "*/whitelist:* `Show current whitelist` \n"
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
@ -1408,8 +1447,8 @@ class Telegram(RPCHandler):
" `pending buy orders are marked with an asterisk (*)`\n" " `pending buy orders are marked with an asterisk (*)`\n"
" `pending sell orders are marked with a double asterisk (**)`\n" " `pending sell orders are marked with a double asterisk (**)`\n"
"*/buys <pair|none>:* `Shows the enter_tag performance`\n" "*/buys <pair|none>:* `Shows the enter_tag performance`\n"
"*/sells <pair|none>:* `Shows the sell reason performance`\n" "*/sells <pair|none>:* `Shows the exit reason performance`\n"
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n" "*/mix_tags <pair|none>:* `Shows combined entry tag + exit reason performance`\n"
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, " "*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
"over the last n days`\n" "over the last n days`\n"

View File

@ -43,23 +43,23 @@ class Webhook(RPCHandler):
def send_msg(self, msg: Dict[str, Any]) -> None: def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """ """ Send a message to telegram channel """
try: try:
whconfig = self._config['webhook']
if msg['type'] in [RPCMessageType.BUY, RPCMessageType.SHORT]: if msg['type'] in [RPCMessageType.ENTRY]:
valuedict = self._config['webhook'].get('webhookbuy', None) valuedict = whconfig.get('webhookentry', None)
elif msg['type'] in [RPCMessageType.BUY_CANCEL, RPCMessageType.SHORT_CANCEL]: elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
valuedict = self._config['webhook'].get('webhookbuycancel', None) valuedict = whconfig.get('webhookentrycancel', None)
elif msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]: elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
valuedict = self._config['webhook'].get('webhookbuyfill', None) valuedict = whconfig.get('webhookentryfill', None)
elif msg['type'] == RPCMessageType.SELL: elif msg['type'] == RPCMessageType.EXIT:
valuedict = self._config['webhook'].get('webhooksell', None) valuedict = whconfig.get('webhookexit', None)
elif msg['type'] == RPCMessageType.SELL_FILL: elif msg['type'] == RPCMessageType.EXIT_FILL:
valuedict = self._config['webhook'].get('webhooksellfill', None) valuedict = whconfig.get('webhookexitfill', None)
elif msg['type'] == RPCMessageType.SELL_CANCEL: elif msg['type'] == RPCMessageType.EXIT_CANCEL:
valuedict = self._config['webhook'].get('webhooksellcancel', None) valuedict = whconfig.get('webhookexitcancel', None)
elif msg['type'] in (RPCMessageType.STATUS, elif msg['type'] in (RPCMessageType.STATUS,
RPCMessageType.STARTUP, RPCMessageType.STARTUP,
RPCMessageType.WARNING): RPCMessageType.WARNING):
valuedict = self._config['webhook'].get('webhookstatus', None) valuedict = whconfig.get('webhookstatus', None)
else: else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
if not valuedict: if not valuedict:

View File

@ -90,10 +90,10 @@ class IStrategy(ABC, HyperStrategyMixin):
# run "populate_indicators" only for new candle # run "populate_indicators" only for new candle
process_only_new_candles: bool = False process_only_new_candles: bool = False
use_sell_signal: bool use_exit_signal: bool
sell_profit_only: bool exit_profit_only: bool
sell_profit_offset: float exit_profit_offset: float
ignore_roi_if_buy_signal: bool ignore_roi_if_entry_signal: bool
# Position adjustment is disabled by default # Position adjustment is disabled by default
position_adjustment_enable: bool = False position_adjustment_enable: bool = False
@ -308,10 +308,10 @@ class IStrategy(ABC, HyperStrategyMixin):
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param exit_reason: Exit reason. :param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell'] 'exit_signal', 'force_exit', 'emergency_exit']
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True, then the sell-order/exit_short-order is placed on the exchange. :return bool: When True, then the exit-order is placed on the exchange.
False aborts the process False aborts the process
""" """
return True return True
@ -339,7 +339,7 @@ class IStrategy(ABC, HyperStrategyMixin):
return self.stoploss return self.stoploss
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
entry_tag: Optional[str], **kwargs) -> float: entry_tag: Optional[str], side: str, **kwargs) -> float:
""" """
Custom entry price logic, returning the new entry price. Custom entry price logic, returning the new entry price.
@ -351,6 +351,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in exit_pricing. :param proposed_rate: Rate, calculated based on pricing settings in exit_pricing.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New entry price value if provided :return float: New entry price value if provided
""" """
@ -396,7 +397,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param current_rate: Rate, calculated based on pricing settings in exit_pricing. :param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: To execute exit, return a string with custom sell reason or True. Otherwise return :return: To execute exit, return a string with custom exit reason or True. Otherwise return
None or False. None or False.
""" """
return None return None
@ -420,7 +421,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param current_rate: Rate, calculated based on pricing settings in exit_pricing. :param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: To execute exit, return a string with custom sell reason or True. Otherwise return :return: To execute exit, return a string with custom exit reason or True. Otherwise return
None or False. None or False.
""" """
return self.custom_sell(pair, trade, current_time, current_rate, current_profit, **kwargs) return self.custom_sell(pair, trade, current_time, current_rate, current_profit, **kwargs)
@ -634,8 +635,6 @@ class IStrategy(ABC, HyperStrategyMixin):
dataframe[SignalTagType.ENTER_TAG.value] = None dataframe[SignalTagType.ENTER_TAG.value] = None
dataframe[SignalTagType.EXIT_TAG.value] = None dataframe[SignalTagType.EXIT_TAG.value] = None
# Other Defs in strategy that want to be called every loop here
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
logger.debug("Loop Analysis Launched") logger.debug("Loop Analysis Launched")
return dataframe return dataframe
@ -716,7 +715,7 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
Calculates current signal based based on the entry order or exit order Calculates current signal based based on the entry order or exit order
columns of the dataframe. columns of the dataframe.
Used by Bot to get the signal to buy, sell, short, or exit_short Used by Bot to get the signal to enter, or exit
:param pair: pair in format ANT/BTC :param pair: pair in format ANT/BTC
:param timeframe: timeframe to use :param timeframe: timeframe to use
:param dataframe: Analyzed dataframe to get signal from. :param dataframe: Analyzed dataframe to get signal from.
@ -750,7 +749,7 @@ class IStrategy(ABC, HyperStrategyMixin):
is_short: bool = None is_short: bool = None
) -> Tuple[bool, bool, Optional[str]]: ) -> Tuple[bool, bool, Optional[str]]:
""" """
Calculates current exit signal based based on the buy/short or sell/exit_short Calculates current exit signal based based on the dataframe
columns of the dataframe. columns of the dataframe.
Used by Bot to get the signal to exit. Used by Bot to get the signal to exit.
depending on is_short, looks at "short" or "long" columns. depending on is_short, looks at "short" or "long" columns.
@ -787,9 +786,9 @@ class IStrategy(ABC, HyperStrategyMixin):
dataframe: DataFrame, dataframe: DataFrame,
) -> Tuple[Optional[SignalDirection], Optional[str]]: ) -> Tuple[Optional[SignalDirection], Optional[str]]:
""" """
Calculates current entry signal based based on the buy/short or sell/exit_short Calculates current entry signal based based on the dataframe signals
columns of the dataframe. columns of the dataframe.
Used by Bot to get the signal to buy, sell, short, or exit_short Used by Bot to get the signal to enter trades.
:param pair: pair in format ANT/BTC :param pair: pair in format ANT/BTC
:param timeframe: timeframe to use :param timeframe: timeframe to use
:param dataframe: Analyzed dataframe to get signal from. :param dataframe: Analyzed dataframe to get signal from.
@ -867,59 +866,59 @@ class IStrategy(ABC, HyperStrategyMixin):
current_profit=current_profit, current_profit=current_profit,
force_stoploss=force_stoploss, low=low, high=high) force_stoploss=force_stoploss, low=low, high=high)
# Set current rate to high for backtesting sell # Set current rate to high for backtesting exits
current_rate = (low if trade.is_short else high) or rate current_rate = (low if trade.is_short else high) or rate
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
# if enter signal and ignore_roi is set, we don't need to evaluate min_roi. # if enter signal and ignore_roi is set, we don't need to evaluate min_roi.
roi_reached = (not (enter and self.ignore_roi_if_buy_signal) roi_reached = (not (enter and self.ignore_roi_if_entry_signal)
and self.min_roi_reached(trade=trade, current_profit=current_profit, and self.min_roi_reached(trade=trade, current_profit=current_profit,
current_time=current_time)) current_time=current_time))
sell_signal = ExitType.NONE exit_signal = ExitType.NONE
custom_reason = '' custom_reason = ''
# use provided rate in backtesting, not high/low. # use provided rate in backtesting, not high/low.
current_rate = rate current_rate = rate
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
if (self.sell_profit_only and current_profit <= self.sell_profit_offset): if self.use_exit_signal:
# sell_profit_only and profit doesn't reach the offset - ignore sell signal if exit_ and not enter:
pass exit_signal = ExitType.EXIT_SIGNAL
elif self.use_sell_signal and not enter:
if exit_:
sell_signal = ExitType.SELL_SIGNAL
else: else:
trade_type = "exit_short" if trade.is_short else "sell"
custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)( custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
pair=trade.pair, trade=trade, current_time=current_time, pair=trade.pair, trade=trade, current_time=current_time,
current_rate=current_rate, current_profit=current_profit) current_rate=current_rate, current_profit=current_profit)
if custom_reason: if custom_reason:
sell_signal = ExitType.CUSTOM_SELL exit_signal = ExitType.CUSTOM_EXIT
if isinstance(custom_reason, str): if isinstance(custom_reason, str):
if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH: if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH:
logger.warning(f'Custom {trade_type} reason returned from ' logger.warning(f'Custom exit reason returned from '
f'custom_exit is too long and was trimmed' f'custom_exit is too long and was trimmed'
f'to {CUSTOM_EXIT_MAX_LENGTH} characters.') f'to {CUSTOM_EXIT_MAX_LENGTH} characters.')
custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH] custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH]
else: else:
custom_reason = None custom_reason = None
if sell_signal in (ExitType.CUSTOM_SELL, ExitType.SELL_SIGNAL): if (
exit_signal == ExitType.CUSTOM_EXIT
or (exit_signal == ExitType.EXIT_SIGNAL
and (not self.exit_profit_only or current_profit > self.exit_profit_offset))
):
logger.debug(f"{trade.pair} - Sell signal received. " logger.debug(f"{trade.pair} - Sell signal received. "
f"sell_type=ExitType.{sell_signal.name}" + f"exit_type=ExitType.{exit_signal.name}" +
(f", custom_reason={custom_reason}" if custom_reason else "")) (f", custom_reason={custom_reason}" if custom_reason else ""))
return ExitCheckTuple(exit_type=sell_signal, exit_reason=custom_reason) return ExitCheckTuple(exit_type=exit_signal, exit_reason=custom_reason)
# Sequence: # Sequence:
# Exit-signal # Exit-signal
# ROI (if not stoploss) # ROI (if not stoploss)
# Stoploss # Stoploss
if roi_reached and stoplossflag.exit_type != ExitType.STOP_LOSS: if roi_reached and stoplossflag.exit_type != ExitType.STOP_LOSS:
logger.debug(f"{trade.pair} - Required profit reached. sell_type=ExitType.ROI") logger.debug(f"{trade.pair} - Required profit reached. exit_type=ExitType.ROI")
return ExitCheckTuple(exit_type=ExitType.ROI) return ExitCheckTuple(exit_type=ExitType.ROI)
if stoplossflag.exit_flag: if stoplossflag.exit_flag:
logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.exit_type}") logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}")
return stoplossflag return stoplossflag
# This one is noisy, commented out... # This one is noisy, commented out...
@ -988,11 +987,11 @@ class IStrategy(ABC, HyperStrategyMixin):
if ((sl_higher_long or sl_lower_short) and if ((sl_higher_long or sl_lower_short) and
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
sell_type = ExitType.STOP_LOSS exit_type = ExitType.STOP_LOSS
# If initial stoploss is not the same as current one then it is trailing. # If initial stoploss is not the same as current one then it is trailing.
if trade.initial_stop_loss != trade.stop_loss: if trade.initial_stop_loss != trade.stop_loss:
sell_type = ExitType.TRAILING_STOP_LOSS exit_type = ExitType.TRAILING_STOP_LOSS
logger.debug( logger.debug(
f"{trade.pair} - HIT STOP: current price at " f"{trade.pair} - HIT STOP: current price at "
f"{((high if trade.is_short else low) or current_rate):.6f}, " f"{((high if trade.is_short else low) or current_rate):.6f}, "
@ -1007,7 +1006,7 @@ class IStrategy(ABC, HyperStrategyMixin):
logger.debug(f"{trade.pair} - Trailing stop saved " logger.debug(f"{trade.pair} - Trailing stop saved "
f"{new_stoploss:.6f}") f"{new_stoploss:.6f}")
return ExitCheckTuple(exit_type=sell_type) return ExitCheckTuple(exit_type=exit_type)
return ExitCheckTuple(exit_type=ExitType.NONE) return ExitCheckTuple(exit_type=ExitType.NONE)
@ -1027,9 +1026,9 @@ class IStrategy(ABC, HyperStrategyMixin):
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
""" """
Based on trade duration, current profit of the trade and ROI configuration, Based on trade duration, current profit of the trade and ROI configuration,
decides whether bot should sell. decides whether bot should exit.
:param current_profit: current profit as ratio :param current_profit: current profit as ratio
:return: True if bot should sell at current rate :return: True if bot should exit at current rate
""" """
# Check if time matches and current rate is above threshold # Check if time matches and current rate is above threshold
trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60) trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60)
@ -1045,7 +1044,7 @@ class IStrategy(ABC, HyperStrategyMixin):
FT Internal method. FT Internal method.
Check if timeout is active, and if the order is still open and timed out Check if timeout is active, and if the order is still open and timed out
""" """
side = 'entry' if order.ft_order_side == trade.enter_side else 'exit' side = 'entry' if order.ft_order_side == trade.entry_side else 'exit'
timeout = self.config.get('unfilledtimeout', {}).get(side) timeout = self.config.get('unfilledtimeout', {}).get(side)
if timeout is not None: if timeout is not None:
@ -1128,7 +1127,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information dictionary, with details like the :param metadata: Additional information dictionary, with details like the
currently traded pair currently traded pair
:return: DataFrame with sell column :return: DataFrame with exit column
""" """
logger.debug(f"Populating exit signals for pair {metadata.get('pair')}.") logger.debug(f"Populating exit signals for pair {metadata.get('pair')}.")

View File

@ -72,7 +72,7 @@
}, },
"bot_name": "freqtrade", "bot_name": "freqtrade",
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false, "force_entry_enable": false,
"internals": { "internals": {
"process_throttle_secs": 5 "process_throttle_secs": 5
} }

View File

@ -65,9 +65,9 @@ class {{ strategy }}(IStrategy):
process_only_new_candles = False process_only_new_candles = False
# These values can be overridden in the config. # These values can be overridden in the config.
use_sell_signal = True use_exit_signal = True
sell_profit_only = False exit_profit_only = False
ignore_roi_if_buy_signal = False ignore_roi_if_entry_signal = False
# Number of candles the strategy requires before producing valid signals # Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 30 startup_candle_count: int = 30

View File

@ -65,9 +65,9 @@ class SampleStrategy(IStrategy):
process_only_new_candles = False process_only_new_candles = False
# These values can be overridden in the config. # These values can be overridden in the config.
use_sell_signal = True use_exit_signal = True
sell_profit_only = False exit_profit_only = False
ignore_roi_if_buy_signal = False ignore_roi_if_entry_signal = False
# Hyperoptable parameters # Hyperoptable parameters
buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)

View File

@ -1,7 +1,7 @@
"order_types": { "order_types": {
"entry": "limit", "entry": "limit",
"exit": "limit", "exit": "limit",
"emergencyexit": "limit", "emergency_exit": "limit",
"stoploss": "limit", "stoploss": "limit",
"stoploss_on_exchange": false "stoploss_on_exchange": false
}, },

View File

@ -95,7 +95,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]': current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]':
""" """
Custom sell signal logic indicating that specified position should be sold. Returning a Custom exit signal logic indicating that specified position should be sold. Returning a
string or True from this method is equal to setting sell signal on a candle at specified string or True from this method is equal to setting sell signal on a candle at specified
time. This method is not called when sell signal is set. time. This method is not called when sell signal is set.
@ -103,7 +103,7 @@ def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', curre
example you could implement a sell relative to the candle when the trade was opened, example you could implement a sell relative to the candle when the trade was opened,
or a custom 1:2 risk-reward ROI. or a custom 1:2 risk-reward ROI.
Custom sell reason max length is 64. Exceeding characters will be removed. Custom exit reason max length is 64. Exceeding characters will be removed.
:param pair: Pair that's currently analyzed :param pair: Pair that's currently analyzed
:param trade: trade object. :param trade: trade object.
@ -111,7 +111,7 @@ def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', curre
:param current_rate: Rate, calculated based on pricing settings in exit_pricing. :param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: To execute sell, return a string with custom sell reason or True. Otherwise return :return: To execute sell, return a string with custom exit reason or True. Otherwise return
None or False. None or False.
""" """
return None return None
@ -162,10 +162,10 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param exit_reason: Exit reason. :param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell'] 'exit_signal', 'force_exit', 'emergency_exit']
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange. :return bool: When True is returned, then the exit-order is placed on the exchange.
False aborts the process False aborts the process
""" """
return True return True
@ -206,7 +206,7 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -
:param trade: trade object. :param trade: trade object.
:param order: Order dictionary as returned from CCXT. :param order: Order dictionary as returned from CCXT.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is cancelled. :return bool: When True is returned, then the exit-order is cancelled.
""" """
return False return False

View File

@ -2,11 +2,13 @@
-r requirements.txt -r requirements.txt
-r requirements-plot.txt -r requirements-plot.txt
-r requirements-hyperopt.txt -r requirements-hyperopt.txt
-r docs/requirements-docs.txt
coveralls==3.3.1 coveralls==3.3.1
flake8==4.0.1 flake8==4.0.1
flake8-tidy-imports==4.6.0 flake8-tidy-imports==4.6.0
mypy==0.942 mypy==0.942
pre-commit==2.18.1
pytest==7.1.1 pytest==7.1.1
pytest-asyncio==0.18.3 pytest-asyncio==0.18.3
pytest-cov==3.0.0 pytest-cov==3.0.0
@ -17,12 +19,12 @@ isort==5.10.1
time-machine==2.6.0 time-machine==2.6.0
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==6.4.4 nbconvert==6.4.5
# mypy types # mypy types
types-cachetools==5.0.0 types-cachetools==5.0.0
types-filelock==3.2.5 types-filelock==3.2.5
types-requests==2.27.15 types-requests==2.27.16
types-tabulate==0.8.6 types-tabulate==0.8.6
# Extensions to datetime library # Extensions to datetime library

View File

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

View File

@ -1,12 +1,12 @@
numpy==1.22.3 numpy==1.22.3
pandas==1.4.1 pandas==1.4.2
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.77.45 ccxt==1.78.62
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==36.0.2 cryptography==36.0.2
aiohttp==3.8.1 aiohttp==3.8.1
SQLAlchemy==1.4.32 SQLAlchemy==1.4.35
python-telegram-bot==13.11 python-telegram-bot==13.11
arrow==1.2.2 arrow==1.2.2
cachetools==4.2.2 cachetools==4.2.2
@ -31,7 +31,7 @@ python-rapidjson==1.6
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.75.0 fastapi==0.75.1
uvicorn==0.17.6 uvicorn==0.17.6
pyjwt==2.3.0 pyjwt==2.3.0
aiofiles==0.8.0 aiofiles==0.8.0
@ -41,7 +41,7 @@ psutil==5.9.0
colorama==0.4.4 colorama==0.4.4
# Building config files interactively # Building config files interactively
questionary==1.10.0 questionary==1.10.0
prompt-toolkit==3.0.28 prompt-toolkit==3.0.29
# Extensions to datetime library # Extensions to datetime library
python-dateutil==2.8.2 python-dateutil==2.8.2

View File

@ -261,7 +261,7 @@ class FtRestClient():
} }
return self._post("forcebuy", data=data) return self._post("forcebuy", data=data)
def forceenter(self, pair, side, price=None): def force_enter(self, pair, side, price=None):
"""Force entering a trade """Force entering a trade
:param pair: Pair to buy (ETH/BTC) :param pair: Pair to buy (ETH/BTC)
@ -273,16 +273,16 @@ class FtRestClient():
"side": side, "side": side,
"price": price, "price": price,
} }
return self._post("forceenter", data=data) return self._post("force_enter", data=data)
def forcesell(self, tradeid): def forceexit(self, tradeid):
"""Force-sell a trade. """Force-exit a trade.
:param tradeid: Id of the trade (can be received via status command) :param tradeid: Id of the trade (can be received via status command)
:return: json object :return: json object
""" """
return self._post("forcesell", data={"tradeid": tradeid}) return self._post("forceexit", data={"tradeid": tradeid})
def strategies(self): def strategies(self):
"""Lists available strategies """Lists available strategies

View File

@ -39,7 +39,9 @@ console_scripts =
freqtrade = freqtrade.main:main freqtrade = freqtrade.main:main
[flake8] [flake8]
#ignore = # Default from https://flake8.pycqa.org/en/latest/user/options.html#cmdoption-flake8-ignore
# minus E226
ignore = E121,E123,E126,E24,E704,W503,W504
max-line-length = 100 max-line-length = 100
max-complexity = 12 max-complexity = 12
exclude = exclude =

View File

@ -51,6 +51,7 @@ function updateenv() {
echo "pip install in-progress. Please wait..." echo "pip install in-progress. Please wait..."
${PYTHON} -m pip install --upgrade pip ${PYTHON} -m pip install --upgrade pip
read -p "Do you want to install dependencies for dev [y/N]? " read -p "Do you want to install dependencies for dev [y/N]? "
dev=$REPLY
if [[ $REPLY =~ ^[Yy]$ ]] if [[ $REPLY =~ ^[Yy]$ ]]
then then
REQUIREMENTS=requirements-dev.txt REQUIREMENTS=requirements-dev.txt
@ -88,6 +89,13 @@ function updateenv() {
fi fi
echo "pip install completed" echo "pip install completed"
echo echo
if [[ $dev =~ ^[Yy]$ ]]; then
${PYTHON} -m pre-commit install
if [ $? -ne 0 ]; then
echo "Failed installing pre-commit"
exit 1
fi
fi
} }
# Install tab lib # Install tab lib

View File

@ -826,8 +826,9 @@ def test_download_data_trades(mocker, caplog):
] ]
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match="Trade download not supported for futures."): match="Trade download not supported for futures."):
pargs = get_args(args)
start_download_data(get_args(args)) pargs['config'] = None
start_download_data(pargs)
def test_start_convert_trades(mocker, caplog): def test_start_convert_trades(mocker, caplog):

View File

@ -87,7 +87,7 @@ def get_mock_coro(return_value):
def patched_configuration_load_config_file(mocker, config) -> None: def patched_configuration_load_config_file(mocker, config) -> None:
mocker.patch( mocker.patch(
'freqtrade.configuration.configuration.load_config_file', 'freqtrade.configuration.load_config.load_config_file',
lambda *args, **kwargs: config lambda *args, **kwargs: config
) )

View File

@ -6,7 +6,7 @@ from freqtrade.persistence.models import Order, Trade
MOCK_TRADE_COUNT = 6 MOCK_TRADE_COUNT = 6
def enter_side(is_short: bool): def entry_side(is_short: bool):
return "sell" if is_short else "buy" return "sell" if is_short else "buy"
@ -23,7 +23,7 @@ def mock_order_1(is_short: bool):
'id': f'1234_{direc(is_short)}', 'id': f'1234_{direc(is_short)}',
'symbol': 'ETH/BTC', 'symbol': 'ETH/BTC',
'status': 'closed', 'status': 'closed',
'side': enter_side(is_short), 'side': entry_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.123, 'price': 0.123,
'average': 0.123, 'average': 0.123,
@ -50,7 +50,7 @@ def mock_trade_1(fee, is_short: bool):
timeframe=5, timeframe=5,
is_short=is_short is_short=is_short
) )
o = Order.parse_from_ccxt_object(mock_order_1(is_short), 'ETH/BTC', enter_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_1(is_short), 'ETH/BTC', entry_side(is_short))
trade.orders.append(o) trade.orders.append(o)
return trade return trade
@ -60,7 +60,7 @@ def mock_order_2(is_short: bool):
'id': f'1235_{direc(is_short)}', 'id': f'1235_{direc(is_short)}',
'symbol': 'ETC/BTC', 'symbol': 'ETC/BTC',
'status': 'closed', 'status': 'closed',
'side': enter_side(is_short), 'side': entry_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
@ -109,7 +109,7 @@ def mock_trade_2(fee, is_short: bool):
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
is_short=is_short is_short=is_short
) )
o = Order.parse_from_ccxt_object(mock_order_2(is_short), 'ETC/BTC', enter_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_2(is_short), 'ETC/BTC', entry_side(is_short))
trade.orders.append(o) trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_2_sell(is_short), 'ETC/BTC', exit_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_2_sell(is_short), 'ETC/BTC', exit_side(is_short))
trade.orders.append(o) trade.orders.append(o)
@ -121,7 +121,7 @@ def mock_order_3(is_short: bool):
'id': f'41231a12a_{direc(is_short)}', 'id': f'41231a12a_{direc(is_short)}',
'symbol': 'XRP/BTC', 'symbol': 'XRP/BTC',
'status': 'closed', 'status': 'closed',
'side': enter_side(is_short), 'side': entry_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.05, 'price': 0.05,
'amount': 123.0, 'amount': 123.0,
@ -169,7 +169,7 @@ def mock_trade_3(fee, is_short: bool):
close_date=datetime.now(tz=timezone.utc), close_date=datetime.now(tz=timezone.utc),
is_short=is_short is_short=is_short
) )
o = Order.parse_from_ccxt_object(mock_order_3(is_short), 'XRP/BTC', enter_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_3(is_short), 'XRP/BTC', entry_side(is_short))
trade.orders.append(o) trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_3_sell(is_short), 'XRP/BTC', exit_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_3_sell(is_short), 'XRP/BTC', exit_side(is_short))
trade.orders.append(o) trade.orders.append(o)
@ -181,7 +181,7 @@ def mock_order_4(is_short: bool):
'id': f'prod_buy_{direc(is_short)}_12345', 'id': f'prod_buy_{direc(is_short)}_12345',
'symbol': 'ETC/BTC', 'symbol': 'ETC/BTC',
'status': 'open', 'status': 'open',
'side': enter_side(is_short), 'side': entry_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
@ -210,7 +210,7 @@ def mock_trade_4(fee, is_short: bool):
timeframe=5, timeframe=5,
is_short=is_short is_short=is_short
) )
o = Order.parse_from_ccxt_object(mock_order_4(is_short), 'ETC/BTC', enter_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_4(is_short), 'ETC/BTC', entry_side(is_short))
trade.orders.append(o) trade.orders.append(o)
return trade return trade
@ -220,7 +220,7 @@ def mock_order_5(is_short: bool):
'id': f'prod_buy_{direc(is_short)}_3455', 'id': f'prod_buy_{direc(is_short)}_3455',
'symbol': 'XRP/BTC', 'symbol': 'XRP/BTC',
'status': 'closed', 'status': 'closed',
'side': enter_side(is_short), 'side': entry_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
@ -264,7 +264,7 @@ def mock_trade_5(fee, is_short: bool):
timeframe=5, timeframe=5,
is_short=is_short is_short=is_short
) )
o = Order.parse_from_ccxt_object(mock_order_5(is_short), 'XRP/BTC', enter_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_5(is_short), 'XRP/BTC', entry_side(is_short))
trade.orders.append(o) trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_5_stoploss(is_short), 'XRP/BTC', 'stoploss') o = Order.parse_from_ccxt_object(mock_order_5_stoploss(is_short), 'XRP/BTC', 'stoploss')
trade.orders.append(o) trade.orders.append(o)
@ -276,7 +276,7 @@ def mock_order_6(is_short: bool):
'id': f'prod_buy_{direc(is_short)}_6', 'id': f'prod_buy_{direc(is_short)}_6',
'symbol': 'LTC/BTC', 'symbol': 'LTC/BTC',
'status': 'closed', 'status': 'closed',
'side': enter_side(is_short), 'side': entry_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.15, 'price': 0.15,
'amount': 2.0, 'amount': 2.0,
@ -320,7 +320,7 @@ def mock_trade_6(fee, is_short: bool):
timeframe=5, timeframe=5,
is_short=is_short is_short=is_short
) )
o = Order.parse_from_ccxt_object(mock_order_6(is_short), 'LTC/BTC', enter_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_6(is_short), 'LTC/BTC', entry_side(is_short))
trade.orders.append(o) trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_6_sell(is_short), 'LTC/BTC', exit_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_6_sell(is_short), 'LTC/BTC', exit_side(is_short))
trade.orders.append(o) trade.orders.append(o)

View File

@ -95,8 +95,8 @@ tc1 = BTContainer(data=[
[6, 5000, 5025, 4975, 4987, 6172, 0, 0], # should sell [6, 5000, 5025, 4975, 4987, 6172, 0, 0], # should sell
], ],
stop_loss=-0.99, roi={"0": float('inf')}, profit_perc=0.00, stop_loss=-0.99, roi={"0": float('inf')}, profit_perc=0.00,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=2), trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=2),
BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=4, close_tick=6)] BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=4, close_tick=6)]
) )
# 3) Entered, sl 1%, candle drops 8% => Trade closed, 1% loss # 3) Entered, sl 1%, candle drops 8% => Trade closed, 1% loss
@ -391,7 +391,7 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc
'trade_duration': '', 'trade_duration': '',
'open_rate': 17, 'open_rate': 17,
'close_rate': 17, 'close_rate': 17,
'exit_type': 'sell_signal'}, 'exit_type': 'exit_signal'},
{'pair': 'TEST/BTC', {'pair': 'TEST/BTC',
'stoploss': -0.9, 'stoploss': -0.9,
@ -402,7 +402,7 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc
'trade_duration': '', 'trade_duration': '',
'open_rate': 20, 'open_rate': 20,
'close_rate': 20, 'close_rate': 20,
'exit_type': 'sell_signal'}, 'exit_type': 'exit_signal'},
{'pair': 'TEST/BTC', {'pair': 'TEST/BTC',
'stoploss': -0.9, 'stoploss': -0.9,
@ -413,7 +413,7 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc
'trade_duration': '', 'trade_duration': '',
'open_rate': 26, 'open_rate': 26,
'close_rate': 34, 'close_rate': 34,
'exit_type': 'sell_signal'} 'exit_type': 'exit_signal'}
] ]
trades_df = DataFrame(trades) trades_df = DataFrame(trades)

View File

@ -2691,7 +2691,8 @@ async def test__async_get_trade_history_time(default_conf, mocker, caplog, excha
# Monkey-patch async function # Monkey-patch async function
exchange._api_async.fetch_trades = MagicMock(side_effect=mock_get_trade_hist) exchange._api_async.fetch_trades = MagicMock(side_effect=mock_get_trade_hist)
pair = 'ETH/BTC' pair = 'ETH/BTC'
ret = await exchange._async_get_trade_history_time(pair, ret = await exchange._async_get_trade_history_time(
pair,
since=fetch_trades_result[0]['timestamp'], since=fetch_trades_result[0]['timestamp'],
until=fetch_trades_result[-1]['timestamp'] - 1) until=fetch_trades_result[-1]['timestamp'] - 1)
assert type(ret) is tuple assert type(ret) is tuple

View File

@ -35,7 +35,7 @@ class BTContainer(NamedTuple):
trailing_only_offset_is_reached: bool = False trailing_only_offset_is_reached: bool = False
trailing_stop_positive: Optional[float] = None trailing_stop_positive: Optional[float] = None
trailing_stop_positive_offset: float = 0.0 trailing_stop_positive_offset: float = 0.0
use_sell_signal: bool = False use_exit_signal: bool = False
use_custom_stoploss: bool = False use_custom_stoploss: bool = False
custom_entry_price: Optional[float] = None custom_entry_price: Optional[float] = None
custom_exit_price: Optional[float] = None custom_exit_price: Optional[float] = None

View File

@ -22,8 +22,8 @@ tc0 = BTContainer(data=[
[3, 5010, 5010, 4980, 5010, 6172, 0, 1], [3, 5010, 5010, 4980, 5010, 6172, 0, 1],
[4, 5010, 5011, 4977, 4995, 6172, 0, 0], [4, 5010, 5011, 4977, 4995, 6172, 0, 0],
[5, 4995, 4995, 4950, 4950, 6172, 0, 0]], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True, stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_exit_signal=True,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 1: Stop-Loss Triggered 1% loss # Test 1: Stop-Loss Triggered 1% loss
@ -251,7 +251,7 @@ tc15 = BTContainer(data=[
BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=2, close_tick=2)] BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=2, close_tick=2)]
) )
# Test 16: Buy, hold for 65 min, then forcesell using roi=-1 # Test 16: Buy, hold for 65 min, then forceexit using roi=-1
# Causes negative profit even though sell-reason is ROI. # Causes negative profit even though sell-reason is ROI.
# stop-loss: 10%, ROI: 10% (should not apply), -100% after 65 minutes (limits trade duration) # stop-loss: 10%, ROI: 10% (should not apply), -100% after 65 minutes (limits trade duration)
tc16 = BTContainer(data=[ tc16 = BTContainer(data=[
@ -259,14 +259,14 @@ tc16 = BTContainer(data=[
[0, 5000, 5025, 4975, 4987, 6172, 1, 0], [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0],
[2, 4987, 5300, 4950, 5050, 6172, 0, 0], [2, 4987, 5300, 4950, 5050, 6172, 0, 0],
[3, 4975, 5000, 4940, 4962, 6172, 0, 0], # ForceSell on ROI (roi=-1) [3, 4975, 5000, 4940, 4962, 6172, 0, 0], # Forceexit on ROI (roi=-1)
[4, 4962, 4987, 4950, 4950, 6172, 0, 0], [4, 4962, 4987, 4950, 4950, 6172, 0, 0],
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10, "65": -1}, profit_perc=-0.012, stop_loss=-0.10, roi={"0": 0.10, "65": -1}, profit_perc=-0.012,
trades=[BTrade(exit_reason=ExitType.ROI, open_tick=1, close_tick=3)] trades=[BTrade(exit_reason=ExitType.ROI, open_tick=1, close_tick=3)]
) )
# Test 17: Buy, hold for 120 mins, then forcesell using roi=-1 # Test 17: Buy, hold for 120 mins, then forceexit using roi=-1
# Causes negative profit even though sell-reason is ROI. # Causes negative profit even though sell-reason is ROI.
# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) # stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration)
# Uses open as sell-rate (special case) - since the roi-time is a multiple of the timeframe. # Uses open as sell-rate (special case) - since the roi-time is a multiple of the timeframe.
@ -275,7 +275,7 @@ tc17 = BTContainer(data=[
[0, 5000, 5025, 4975, 4987, 6172, 1, 0], [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0],
[2, 4987, 5300, 4950, 5050, 6172, 0, 0], [2, 4987, 5300, 4950, 5050, 6172, 0, 0],
[3, 4980, 5000, 4940, 4962, 6172, 0, 0], # ForceSell on ROI (roi=-1) [3, 4980, 5000, 4940, 4962, 6172, 0, 0], # Forceexit on ROI (roi=-1)
[4, 4962, 4987, 4950, 4950, 6172, 0, 0], [4, 4962, 4987, 4950, 4950, 6172, 0, 0],
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10, "120": -1}, profit_perc=-0.004, stop_loss=-0.10, roi={"0": 0.10, "120": -1}, profit_perc=-0.004,
@ -408,7 +408,7 @@ tc25 = BTContainer(data=[
[3, 5010, 5010, 4855, 5010, 6172, 0, 1], # Triggers stoploss + sellsignal [3, 5010, 5010, 4855, 5010, 6172, 0, 1], # Triggers stoploss + sellsignal
[4, 5010, 5010, 4977, 4995, 6172, 0, 0], [4, 5010, 5010, 4977, 4995, 6172, 0, 0],
[5, 4995, 4995, 4950, 4950, 6172, 0, 0]], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, use_sell_signal=True, stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, use_exit_signal=True,
trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=3)] trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=3)]
) )
@ -423,8 +423,8 @@ tc26 = BTContainer(data=[
[3, 5010, 5010, 4986, 5010, 6172, 0, 1], [3, 5010, 5010, 4986, 5010, 6172, 0, 1],
[4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on [4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on
[5, 4995, 4995, 4950, 4950, 6172, 0, 0]], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True, stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_exit_signal=True,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 27: (copy of test26 with leverage) # Test 27: (copy of test26 with leverage)
@ -439,9 +439,9 @@ tc27 = BTContainer(data=[
[3, 5010, 5010, 4986, 5010, 6172, 0, 1], [3, 5010, 5010, 4986, 5010, 6172, 0, 1],
[4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on [4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on
[5, 4995, 4995, 4950, 4950, 6172, 0, 0]], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True, stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_exit_signal=True,
leverage=5.0, leverage=5.0,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 28: (copy of test26 with leverage and as short) # Test 28: (copy of test26 with leverage and as short)
@ -456,9 +456,9 @@ tc28 = BTContainer(data=[
[3, 5010, 5010, 4986, 5010, 6172, 0, 0, 0, 1], [3, 5010, 5010, 4986, 5010, 6172, 0, 0, 0, 1],
[4, 4990, 5010, 4855, 4995, 6172, 0, 0, 0, 0], # Triggers stoploss + sellsignal acted on [4, 4990, 5010, 4855, 4995, 6172, 0, 0, 0, 0], # Triggers stoploss + sellsignal acted on
[5, 4995, 4995, 4950, 4950, 6172, 0, 0, 0, 0]], [5, 4995, 4995, 4950, 4950, 6172, 0, 0, 0, 0]],
stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True, stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_exit_signal=True,
leverage=5.0, leverage=5.0,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4, is_short=True)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=True)]
) )
# Test 29: Sell with signal sell in candle 3 (ROI at signal candle) # Test 29: Sell with signal sell in candle 3 (ROI at signal candle)
# Stoploss at 10% (irrelevant), ROI at 5% (will trigger) # Stoploss at 10% (irrelevant), ROI at 5% (will trigger)
@ -471,7 +471,7 @@ tc29 = BTContainer(data=[
[3, 5010, 5251, 4986, 5010, 6172, 0, 1], # Triggers ROI, sell-signal [3, 5010, 5251, 4986, 5010, 6172, 0, 1], # Triggers ROI, sell-signal
[4, 5010, 5010, 4855, 4995, 6172, 0, 0], [4, 5010, 5010, 4855, 4995, 6172, 0, 0],
[5, 4995, 4995, 4950, 4950, 6172, 0, 0]], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True, stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_exit_signal=True,
trades=[BTrade(exit_reason=ExitType.ROI, open_tick=1, close_tick=3)] trades=[BTrade(exit_reason=ExitType.ROI, open_tick=1, close_tick=3)]
) )
@ -485,8 +485,8 @@ tc30 = BTContainer(data=[
[3, 5010, 5012, 4986, 5010, 6172, 0, 1], # sell-signal [3, 5010, 5012, 4986, 5010, 6172, 0, 1], # sell-signal
[4, 5010, 5251, 4855, 4995, 6172, 0, 0], # Triggers ROI, sell-signal acted on [4, 5010, 5251, 4855, 4995, 6172, 0, 0], # Triggers ROI, sell-signal acted on
[5, 4995, 4995, 4950, 4950, 6172, 0, 0]], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.002, use_sell_signal=True, stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.002, use_exit_signal=True,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 31: trailing_stop should raise so candle 3 causes a stoploss # Test 31: trailing_stop should raise so candle 3 causes a stoploss
@ -706,9 +706,9 @@ tc44 = BTContainer(data=[
[3, 5100, 5100, 4950, 4950, 6172, 0, 0], [3, 5100, 5100, 4950, 4950, 6172, 0, 0],
[4, 5000, 5100, 4950, 4950, 6172, 0, 0]], [4, 5000, 5100, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01,
use_sell_signal=True, use_exit_signal=True,
custom_exit_price=4552, custom_exit_price=4552,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=3)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=3)]
) )
# Test 45: Custom exit price above all candles # Test 45: Custom exit price above all candles
@ -721,9 +721,9 @@ tc45 = BTContainer(data=[
[3, 5100, 5100, 4950, 4950, 6172, 0, 0], [3, 5100, 5100, 4950, 4950, 6172, 0, 0],
[4, 5000, 5100, 4950, 4950, 6172, 0, 0]], [4, 5000, 5100, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0,
use_sell_signal=True, use_exit_signal=True,
custom_exit_price=6052, custom_exit_price=6052,
trades=[BTrade(exit_reason=ExitType.FORCE_SELL, open_tick=1, close_tick=4)] trades=[BTrade(exit_reason=ExitType.FORCE_EXIT, open_tick=1, close_tick=4)]
) )
# Test 46: (Short of tc45) Custom short exit price above below candles # Test 46: (Short of tc45) Custom short exit price above below candles
@ -736,9 +736,9 @@ tc46 = BTContainer(data=[
[3, 5100, 5100, 4950, 4950, 6172, 0, 0, 0, 0], [3, 5100, 5100, 4950, 4950, 6172, 0, 0, 0, 0],
[4, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 0]], [4, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0,
use_sell_signal=True, use_exit_signal=True,
custom_exit_price=4700, custom_exit_price=4700,
trades=[BTrade(exit_reason=ExitType.FORCE_SELL, open_tick=1, close_tick=4, is_short=True)] trades=[BTrade(exit_reason=ExitType.FORCE_EXIT, open_tick=1, close_tick=4, is_short=True)]
) )
# Test 47: Colliding long and short signal # Test 47: Colliding long and short signal
@ -750,7 +750,7 @@ tc47 = BTContainer(data=[
[3, 5100, 5100, 4950, 4950, 6172, 0, 0, 0, 0], [3, 5100, 5100, 4950, 4950, 6172, 0, 0, 0, 0],
[4, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 0]], [4, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0,
use_sell_signal=True, use_exit_signal=True,
trades=[] trades=[]
) )
@ -808,7 +808,7 @@ TESTS = [
@pytest.mark.parametrize("data", TESTS) @pytest.mark.parametrize("data", TESTS)
def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer) -> None:
""" """
run functional tests run functional tests
""" """
@ -821,7 +821,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
if data.trailing_stop_positive is not None: if data.trailing_stop_positive is not None:
default_conf["trailing_stop_positive"] = data.trailing_stop_positive default_conf["trailing_stop_positive"] = data.trailing_stop_positive
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
default_conf["use_sell_signal"] = data.use_sell_signal default_conf["use_exit_signal"] = data.use_exit_signal
mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0) mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)

View File

@ -504,7 +504,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
def test_backtest__enter_trade(default_conf, fee, mocker) -> None: def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
default_conf['use_sell_signal'] = False default_conf['use_exit_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@ -563,7 +563,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
default_conf_usdt['use_sell_signal'] = False default_conf_usdt['use_exit_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@ -645,7 +645,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
default_conf['use_sell_signal'] = False default_conf['use_exit_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@ -740,7 +740,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_sell_signal'] = False default_conf['use_exit_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@ -807,7 +807,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_sell_signal'] = False default_conf['use_exit_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@ -833,7 +833,7 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_sell_signal'] = False default_conf['use_exit_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@ -878,7 +878,7 @@ def test_processed(default_conf, mocker, testdatadir) -> None:
def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_sell_signal'] = False default_conf['use_exit_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=100000) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=100000)
@ -1151,10 +1151,10 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
default_conf.update({ default_conf.update({
"use_sell_signal": True, "use_exit_signal": True,
"sell_profit_only": False, "exit_profit_only": False,
"sell_profit_offset": 0.0, "exit_profit_offset": 0.0,
"ignore_roi_if_buy_signal": False, "ignore_roi_if_entry_signal": False,
}) })
patch_exchange(mocker) patch_exchange(mocker)
backtestmock = MagicMock(return_value={ backtestmock = MagicMock(return_value={
@ -1228,10 +1228,10 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
@pytest.mark.filterwarnings("ignore:deprecated") @pytest.mark.filterwarnings("ignore:deprecated")
def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys): def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys):
default_conf.update({ default_conf.update({
"use_sell_signal": True, "use_exit_signal": True,
"sell_profit_only": False, "exit_profit_only": False,
"sell_profit_offset": 0.0, "exit_profit_offset": 0.0,
"ignore_roi_if_buy_signal": False, "ignore_roi_if_entry_signal": False,
}) })
patch_exchange(mocker) patch_exchange(mocker)
result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'],
@ -1346,10 +1346,10 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
default_conf_usdt.update({ default_conf_usdt.update({
"trading_mode": "futures", "trading_mode": "futures",
"margin_mode": "isolated", "margin_mode": "isolated",
"use_sell_signal": True, "use_exit_signal": True,
"sell_profit_only": False, "exit_profit_only": False,
"sell_profit_offset": 0.0, "exit_profit_offset": 0.0,
"ignore_roi_if_buy_signal": False, "ignore_roi_if_entry_signal": False,
"strategy": CURRENT_TEST_STRATEGY, "strategy": CURRENT_TEST_STRATEGY,
}) })
patch_exchange(mocker) patch_exchange(mocker)
@ -1450,10 +1450,10 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
caplog, testdatadir, capsys): caplog, testdatadir, capsys):
# Tests detail-data loading # Tests detail-data loading
default_conf.update({ default_conf.update({
"use_sell_signal": True, "use_exit_signal": True,
"sell_profit_only": False, "exit_profit_only": False,
"sell_profit_offset": 0.0, "exit_profit_offset": 0.0,
"ignore_roi_if_buy_signal": False, "ignore_roi_if_entry_signal": False,
}) })
patch_exchange(mocker) patch_exchange(mocker)
result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'],
@ -1557,10 +1557,10 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testdatadir, run_id, def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testdatadir, run_id,
start_delta, cache): start_delta, cache):
default_conf.update({ default_conf.update({
"use_sell_signal": True, "use_exit_signal": True,
"sell_profit_only": False, "exit_profit_only": False,
"sell_profit_offset": 0.0, "exit_profit_offset": 0.0,
"ignore_roi_if_buy_signal": False, "ignore_roi_if_entry_signal": False,
}) })
patch_exchange(mocker) patch_exchange(mocker)
backtestmock = MagicMock(return_value={ backtestmock = MagicMock(return_value={

View File

@ -14,7 +14,7 @@ from tests.conftest import patch_exchange
def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_sell_signal'] = False default_conf['use_exit_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))

View File

@ -358,7 +358,7 @@ def test_hyperopt_format_results(hyperopt):
"is_short": [False, False, False, False], "is_short": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01], "stake_amount": [0.01, 0.01, 0.01, 0.01],
"exit_reason": [ExitType.ROI, ExitType.STOP_LOSS, "exit_reason": [ExitType.ROI, ExitType.STOP_LOSS,
ExitType.ROI, ExitType.FORCE_SELL] ExitType.ROI, ExitType.FORCE_EXIT]
}), }),
'config': hyperopt.config, 'config': hyperopt.config,
'locks': [], 'locks': [],
@ -429,7 +429,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
"is_short": [False, False, False, False], "is_short": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01], "stake_amount": [0.01, 0.01, 0.01, 0.01],
"exit_reason": [ExitType.ROI, ExitType.STOP_LOSS, "exit_reason": [ExitType.ROI, ExitType.STOP_LOSS,
ExitType.ROI, ExitType.FORCE_SELL] ExitType.ROI, ExitType.FORCE_EXIT]
}), }),
'config': hyperopt_conf, 'config': hyperopt_conf,
'locks': [], 'locks': [],

View File

@ -77,7 +77,7 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
"is_short": [False, False, False, False], "is_short": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01], "stake_amount": [0.01, 0.01, 0.01, 0.01],
"exit_reason": [ExitType.ROI, ExitType.STOP_LOSS, "exit_reason": [ExitType.ROI, ExitType.STOP_LOSS,
ExitType.ROI, ExitType.FORCE_SELL] ExitType.ROI, ExitType.FORCE_EXIT]
}), }),
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],
@ -129,7 +129,7 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
"is_short": [False, False, False, False], "is_short": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01], "stake_amount": [0.01, 0.01, 0.01, 0.01],
"exit_reason": [ExitType.ROI, ExitType.ROI, "exit_reason": [ExitType.ROI, ExitType.ROI,
ExitType.STOP_LOSS, ExitType.FORCE_SELL] ExitType.STOP_LOSS, ExitType.FORCE_EXIT]
}), }),
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],

View File

@ -11,7 +11,7 @@ from tests.conftest import get_patched_freqtradebot, log_has_re
def generate_mock_trade(pair: str, fee: float, is_open: bool, def generate_mock_trade(pair: str, fee: float, is_open: bool,
sell_reason: str = ExitType.SELL_SIGNAL, sell_reason: str = ExitType.EXIT_SIGNAL,
min_ago_open: int = None, min_ago_close: int = None, min_ago_open: int = None, min_ago_close: int = None,
profit_rate: float = 0.9 profit_rate: float = 0.9
): ):

View File

@ -52,7 +52,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
assert results[0] == { assert results[0] == {
'trade_id': 1, 'trade_id': 1,
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'base_currency': 'BTC', 'base_currency': 'ETH',
'quote_currency': 'BTC',
'open_date': ANY, 'open_date': ANY,
'open_timestamp': ANY, 'open_timestamp': ANY,
'is_open': ANY, 'is_open': ANY,
@ -67,7 +68,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'close_rate_requested': ANY, 'close_rate_requested': ANY,
'sell_reason': ANY, 'sell_reason': ANY,
'exit_reason': ANY, 'exit_reason': ANY,
'sell_order_status': ANY, 'exit_order_status': ANY,
'min_rate': ANY, 'min_rate': ANY,
'max_rate': ANY, 'max_rate': ANY,
'strategy': ANY, 'strategy': ANY,
@ -135,7 +136,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
assert results[0] == { assert results[0] == {
'trade_id': 1, 'trade_id': 1,
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'base_currency': 'BTC', 'base_currency': 'ETH',
'quote_currency': 'BTC',
'open_date': ANY, 'open_date': ANY,
'open_timestamp': ANY, 'open_timestamp': ANY,
'is_open': ANY, 'is_open': ANY,
@ -150,7 +152,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'close_rate_requested': ANY, 'close_rate_requested': ANY,
'sell_reason': ANY, 'sell_reason': ANY,
'exit_reason': ANY, 'exit_reason': ANY,
'sell_order_status': ANY, 'exit_order_status': ANY,
'min_rate': ANY, 'min_rate': ANY,
'max_rate': ANY, 'max_rate': ANY,
'strategy': ANY, 'strategy': ANY,
@ -771,7 +773,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None:
assert freqtradebot.config['max_open_trades'] == 0 assert freqtradebot.config['max_open_trades'] == 0
def test_rpc_forceexit(default_conf, ticker, fee, mocker) -> None: def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
@ -798,29 +800,29 @@ def test_rpc_forceexit(default_conf, ticker, fee, mocker) -> None:
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
with pytest.raises(RPCException, match=r'.*trader is not running*'): with pytest.raises(RPCException, match=r'.*trader is not running*'):
rpc._rpc_forceexit(None) rpc._rpc_force_exit(None)
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
with pytest.raises(RPCException, match=r'.*invalid argument*'): with pytest.raises(RPCException, match=r'.*invalid argument*'):
rpc._rpc_forceexit(None) rpc._rpc_force_exit(None)
msg = rpc._rpc_forceexit('all') msg = rpc._rpc_force_exit('all')
assert msg == {'result': 'Created sell orders for all open trades.'} assert msg == {'result': 'Created sell orders for all open trades.'}
freqtradebot.enter_positions() freqtradebot.enter_positions()
msg = rpc._rpc_forceexit('all') msg = rpc._rpc_force_exit('all')
assert msg == {'result': 'Created sell orders for all open trades.'} assert msg == {'result': 'Created sell orders for all open trades.'}
freqtradebot.enter_positions() freqtradebot.enter_positions()
msg = rpc._rpc_forceexit('2') msg = rpc._rpc_force_exit('2')
assert msg == {'result': 'Created sell order for trade 2.'} assert msg == {'result': 'Created sell order for trade 2.'}
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
with pytest.raises(RPCException, match=r'.*trader is not running*'): with pytest.raises(RPCException, match=r'.*trader is not running*'):
rpc._rpc_forceexit(None) rpc._rpc_force_exit(None)
with pytest.raises(RPCException, match=r'.*trader is not running*'): with pytest.raises(RPCException, match=r'.*trader is not running*'):
rpc._rpc_forceexit('all') rpc._rpc_force_exit('all')
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
@ -849,7 +851,7 @@ def test_rpc_forceexit(default_conf, ticker, fee, mocker) -> None:
) )
# check that the trade is called, which is done by ensuring exchange.cancel_order is called # check that the trade is called, which is done by ensuring exchange.cancel_order is called
# and trade amount is updated # and trade amount is updated
rpc._rpc_forceexit('3') rpc._rpc_force_exit('3')
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert trade.amount == filled_amount assert trade.amount == filled_amount
@ -877,7 +879,7 @@ def test_rpc_forceexit(default_conf, ticker, fee, mocker) -> None:
} }
) )
# check that the trade is called, which is done by ensuring exchange.cancel_order is called # check that the trade is called, which is done by ensuring exchange.cancel_order is called
msg = rpc._rpc_forceexit('4') msg = rpc._rpc_force_exit('4')
assert msg == {'result': 'Created sell order for trade 4.'} assert msg == {'result': 'Created sell order for trade 4.'}
assert cancel_order_mock.call_count == 2 assert cancel_order_mock.call_count == 2
assert trade.amount == amount assert trade.amount == amount
@ -894,7 +896,7 @@ def test_rpc_forceexit(default_conf, ticker, fee, mocker) -> None:
'filled': 0.0 'filled': 0.0
} }
) )
msg = rpc._rpc_forceexit('3') msg = rpc._rpc_force_exit('3')
assert msg == {'result': 'Created sell order for trade 3.'} assert msg == {'result': 'Created sell order for trade 3.'}
# status quo, no exchange calls # status quo, no exchange calls
assert cancel_order_mock.call_count == 3 assert cancel_order_mock.call_count == 3
@ -1182,8 +1184,8 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
assert counts["current"] == 1 assert counts["current"] == 1
def test_rpc_forceentry(mocker, default_conf, ticker, fee, limit_buy_order_open) -> None: def test_rpc_force_entry(mocker, default_conf, ticker, fee, limit_buy_order_open) -> None:
default_conf['forcebuy_enable'] = True default_conf['force_entry_enable'] = True
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
buy_mm = MagicMock(return_value=limit_buy_order_open) buy_mm = MagicMock(return_value=limit_buy_order_open)
mocker.patch.multiple( mocker.patch.multiple(
@ -1221,7 +1223,7 @@ def test_rpc_forceentry(mocker, default_conf, ticker, fee, limit_buy_order_open)
pair = 'LTC/BTC' pair = 'LTC/BTC'
trade = rpc._rpc_force_entry(pair, 0.0001, order_type='limit', stake_amount=0.05) trade = rpc._rpc_force_entry(pair, 0.0001, order_type='limit', stake_amount=0.05)
assert trade.stake_amount == 0.05 assert trade.stake_amount == 0.05
assert trade.buy_tag == 'forceentry' assert trade.buy_tag == 'force_entry'
# Test not buying # Test not buying
pair = 'XRP/BTC' pair = 'XRP/BTC'
@ -1230,12 +1232,12 @@ def test_rpc_forceentry(mocker, default_conf, ticker, fee, limit_buy_order_open)
patch_get_signal(freqtradebot) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
pair = 'TKN/BTC' pair = 'TKN/BTC'
with pytest.raises(RPCException, match=r"Failed to enter position for TKN/BTC."):
trade = rpc._rpc_force_entry(pair, None) trade = rpc._rpc_force_entry(pair, None)
assert trade is None
def test_rpc_forceentry_stopped(mocker, default_conf) -> None: def test_rpc_force_entry_stopped(mocker, default_conf) -> None:
default_conf['forcebuy_enable'] = True default_conf['force_entry_enable'] = True
default_conf['initial_state'] = 'stopped' default_conf['initial_state'] = 'stopped'
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
@ -1247,19 +1249,19 @@ def test_rpc_forceentry_stopped(mocker, default_conf) -> None:
rpc._rpc_force_entry(pair, None) rpc._rpc_force_entry(pair, None)
def test_rpc_forceentry_disabled(mocker, default_conf) -> None: def test_rpc_force_entry_disabled(mocker, default_conf) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
pair = 'ETH/BTC' pair = 'ETH/BTC'
with pytest.raises(RPCException, match=r'Forceentry not enabled.'): with pytest.raises(RPCException, match=r'Force_entry not enabled.'):
rpc._rpc_force_entry(pair, None) rpc._rpc_force_entry(pair, None)
def test_rpc_forceentry_wrong_mode(mocker, default_conf) -> None: def test_rpc_force_entry_wrong_mode(mocker, default_conf) -> None:
default_conf['forcebuy_enable'] = True default_conf['force_entry_enable'] = True
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)

View File

@ -931,6 +931,8 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
'open_order': None, 'open_order': None,
'open_rate': 0.123, 'open_rate': 0.123,
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'base_currency': 'ETH',
'quote_currency': 'BTC',
'stake_amount': 0.001, 'stake_amount': 0.001,
'stop_loss_abs': ANY, 'stop_loss_abs': ANY,
'stop_loss_pct': ANY, 'stop_loss_pct': ANY,
@ -963,7 +965,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
'open_trade_value': open_trade_value, 'open_trade_value': open_trade_value,
'sell_reason': None, 'sell_reason': None,
'exit_reason': None, 'exit_reason': None,
'sell_order_status': None, 'exit_order_status': None,
'strategy': CURRENT_TEST_STRATEGY, 'strategy': CURRENT_TEST_STRATEGY,
'buy_tag': None, 'buy_tag': None,
'enter_tag': None, 'enter_tag': None,
@ -1077,16 +1079,16 @@ def test_api_whitelist(botclient):
'forcebuy', 'forcebuy',
'forceenter', 'forceenter',
]) ])
def test_api_forceentry(botclient, mocker, fee, endpoint): def test_api_force_entry(botclient, mocker, fee, endpoint):
ftbot, client = botclient ftbot, client = botclient
rc = client_post(client, f"{BASE_URI}/{endpoint}", rc = client_post(client, f"{BASE_URI}/{endpoint}",
data='{"pair": "ETH/BTC"}') data='{"pair": "ETH/BTC"}')
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json() == {"error": f"Error querying /api/v1/{endpoint}: Forceentry not enabled."} assert rc.json() == {"error": f"Error querying /api/v1/{endpoint}: Force_entry not enabled."}
# enable forcebuy # enable forcebuy
ftbot.config['forcebuy_enable'] = True ftbot.config['force_entry_enable'] = True
fbuy_mock = MagicMock(return_value=None) fbuy_mock = MagicMock(return_value=None)
mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock) mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock)
@ -1097,7 +1099,7 @@ def test_api_forceentry(botclient, mocker, fee, endpoint):
# Test creating trade # Test creating trade
fbuy_mock = MagicMock(return_value=Trade( fbuy_mock = MagicMock(return_value=Trade(
pair='ETH/ETH', pair='ETH/BTC',
amount=1, amount=1,
amount_requested=1, amount_requested=1,
exchange='binance', exchange='binance',
@ -1130,7 +1132,9 @@ def test_api_forceentry(botclient, mocker, fee, endpoint):
'open_date': ANY, 'open_date': ANY,
'open_timestamp': ANY, 'open_timestamp': ANY,
'open_rate': 0.245441, 'open_rate': 0.245441,
'pair': 'ETH/ETH', 'pair': 'ETH/BTC',
'base_currency': 'ETH',
'quote_currency': 'BTC',
'stake_amount': 1, 'stake_amount': 1,
'stop_loss_abs': None, 'stop_loss_abs': None,
'stop_loss_pct': None, 'stop_loss_pct': None,
@ -1164,7 +1168,7 @@ def test_api_forceentry(botclient, mocker, fee, endpoint):
'open_trade_value': 0.24605460, 'open_trade_value': 0.24605460,
'sell_reason': None, 'sell_reason': None,
'exit_reason': None, 'exit_reason': None,
'sell_order_status': None, 'exit_order_status': None,
'strategy': CURRENT_TEST_STRATEGY, 'strategy': CURRENT_TEST_STRATEGY,
'buy_tag': None, 'buy_tag': None,
'enter_tag': None, 'enter_tag': None,
@ -1178,7 +1182,7 @@ def test_api_forceentry(botclient, mocker, fee, endpoint):
} }
def test_api_forcesell(botclient, mocker, ticker, fee, markets): def test_api_forceexit(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -1190,15 +1194,15 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
) )
patch_get_signal(ftbot) patch_get_signal(ftbot)
rc = client_post(client, f"{BASE_URI}/forcesell", rc = client_post(client, f"{BASE_URI}/forceexit",
data='{"tradeid": "1"}') data='{"tradeid": "1"}')
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json() == {"error": "Error querying /api/v1/forcesell: invalid argument"} assert rc.json() == {"error": "Error querying /api/v1/forceexit: invalid argument"}
Trade.query.session.rollback() Trade.query.session.rollback()
ftbot.enter_positions() ftbot.enter_positions()
rc = client_post(client, f"{BASE_URI}/forcesell", rc = client_post(client, f"{BASE_URI}/forceexit",
data='{"tradeid": "1"}') data='{"tradeid": "1"}')
assert_response(rc) assert_response(rc)
assert rc.json() == {'result': 'Created sell order for trade 1.'} assert rc.json() == {'result': 'Created sell order for trade 1.'}

View File

@ -95,7 +95,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
"['balance'], ['start'], ['stop'], " "['balance'], ['start'], ['stop'], "
"['forcesell', 'forceexit'], ['forcebuy', 'forcelong'], ['forceshort'], " "['forcesell', 'forceexit', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], "
"['trades'], ['delete'], ['performance'], " "['trades'], ['delete'], ['performance'], "
"['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], " "['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], "
"['stats'], ['daily'], ['weekly'], ['monthly'], " "['stats'], ['daily'], ['weekly'], ['monthly'], "
@ -184,7 +184,8 @@ def test_telegram_status(default_conf, update, mocker) -> None:
_rpc_trade_status=MagicMock(return_value=[{ _rpc_trade_status=MagicMock(return_value=[{
'trade_id': 1, 'trade_id': 1,
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'base_currency': 'BTC', 'base_currency': 'ETH',
'quote_currency': 'BTC',
'open_date': arrow.utcnow(), 'open_date': arrow.utcnow(),
'close_date': None, 'close_date': None,
'open_rate': 1.099e-05, 'open_rate': 1.099e-05,
@ -199,7 +200,7 @@ def test_telegram_status(default_conf, update, mocker) -> None:
'profit_ratio': -0.0059, 'profit_ratio': -0.0059,
'initial_stop_loss_abs': 1.098e-05, 'initial_stop_loss_abs': 1.098e-05,
'stop_loss_abs': 1.099e-05, 'stop_loss_abs': 1.099e-05,
'sell_order_status': None, 'exit_order_status': None,
'initial_stop_loss_ratio': -0.0005, 'initial_stop_loss_ratio': -0.0005,
'stoploss_current_dist': 1e-08, 'stoploss_current_dist': 1e-08,
'stoploss_current_dist_ratio': -0.0002, 'stoploss_current_dist_ratio': -0.0002,
@ -398,8 +399,8 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ') fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
assert int(fields[0]) == 1 assert int(fields[0]) == 1
assert 'L' in fields[1] # assert 'L' in fields[1]
assert 'ETH/BTC' in fields[2] assert 'ETH/BTC' in fields[1]
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -1004,7 +1005,7 @@ def test_reload_config_handle(default_conf, update, mocker) -> None:
assert 'Reloading config' in msg_mock.call_args_list[0][0][0] assert 'Reloading config' in msg_mock.call_args_list[0][0][0]
def test_telegram_forcesell_handle(default_conf, update, ticker, fee, def test_telegram_forceexit_handle(default_conf, update, ticker, fee,
ticker_sell_up, mocker) -> None: ticker_sell_up, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
@ -1032,15 +1033,15 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
# Increase the price and sell it # Increase the price and sell it
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up)
# /forcesell 1 # /forceexit 1
context = MagicMock() context = MagicMock()
context.args = ["1"] context.args = ["1"]
telegram._forceexit(update=update, context=context) telegram._force_exit(update=update, context=context)
assert msg_mock.call_count == 4 assert msg_mock.call_count == 4
last_msg = msg_mock.call_args_list[-2][0][0] last_msg = msg_mock.call_args_list[-2][0][0]
assert { assert {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
@ -1059,15 +1060,15 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'buy_tag': ANY, 'buy_tag': ANY,
'enter_tag': ANY, 'enter_tag': ANY,
'sell_reason': ExitType.FORCE_SELL.value, 'sell_reason': ExitType.FORCE_EXIT.value,
'exit_reason': ExitType.FORCE_SELL.value, 'exit_reason': ExitType.FORCE_EXIT.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
} == last_msg } == last_msg
def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee,
ticker_sell_down, mocker) -> None: ticker_sell_down, mocker) -> None:
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
return_value=15000.0) return_value=15000.0)
@ -1100,16 +1101,16 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
# /forcesell 1 # /forceexit 1
context = MagicMock() context = MagicMock()
context.args = ["1"] context.args = ["1"]
telegram._forceexit(update=update, context=context) telegram._force_exit(update=update, context=context)
assert msg_mock.call_count == 4 assert msg_mock.call_count == 4
last_msg = msg_mock.call_args_list[-2][0][0] last_msg = msg_mock.call_args_list[-2][0][0]
assert { assert {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
@ -1128,15 +1129,15 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'buy_tag': ANY, 'buy_tag': ANY,
'enter_tag': ANY, 'enter_tag': ANY,
'sell_reason': ExitType.FORCE_SELL.value, 'sell_reason': ExitType.FORCE_EXIT.value,
'exit_reason': ExitType.FORCE_SELL.value, 'exit_reason': ExitType.FORCE_EXIT.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
} == last_msg } == last_msg
def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None: def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None:
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
return_value=15000.0) return_value=15000.0)
@ -1159,16 +1160,16 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
freqtradebot.enter_positions() freqtradebot.enter_positions()
msg_mock.reset_mock() msg_mock.reset_mock()
# /forcesell all # /forceexit all
context = MagicMock() context = MagicMock()
context.args = ["all"] context.args = ["all"]
telegram._forceexit(update=update, context=context) telegram._force_exit(update=update, context=context)
# Called for each trade 2 times # Called for each trade 2 times
assert msg_mock.call_count == 8 assert msg_mock.call_count == 8
msg = msg_mock.call_args_list[0][0][0] msg = msg_mock.call_args_list[0][0][0]
assert { assert {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
@ -1187,15 +1188,15 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'buy_tag': ANY, 'buy_tag': ANY,
'enter_tag': ANY, 'enter_tag': ANY,
'sell_reason': ExitType.FORCE_SELL.value, 'sell_reason': ExitType.FORCE_EXIT.value,
'exit_reason': ExitType.FORCE_SELL.value, 'exit_reason': ExitType.FORCE_EXIT.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
} == msg } == msg
def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: def test_forceexit_handle_invalid(default_conf, update, mocker) -> None:
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
return_value=15000.0) return_value=15000.0)
@ -1204,34 +1205,78 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
# Trader is not running # Trader is not running
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
# /forcesell 1 # /forceexit 1
context = MagicMock() context = MagicMock()
context.args = ["1"] context.args = ["1"]
telegram._forceexit(update=update, context=context) telegram._force_exit(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0] assert 'not running' in msg_mock.call_args_list[0][0][0]
# No argument
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
context = MagicMock()
context.args = []
telegram._forceexit(update=update, context=context)
assert msg_mock.call_count == 1
assert "You must specify a trade-id or 'all'." in msg_mock.call_args_list[0][0][0]
# Invalid argument # Invalid argument
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
# /forcesell 123456 # /forceexit 123456
context = MagicMock() context = MagicMock()
context.args = ["123456"] context.args = ["123456"]
telegram._forceexit(update=update, context=context) telegram._force_exit(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'invalid argument' in msg_mock.call_args_list[0][0][0] assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
def test_forceenter_handle(default_conf, update, mocker) -> None: def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None:
default_conf['max_open_trades'] = 4
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
_is_dry_limit_order_filled=MagicMock(return_value=True),
)
femock = mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_exit')
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# /forceexit
context = MagicMock()
context.args = []
telegram._force_exit(update=update, context=context)
# No pair
assert msg_mock.call_args_list[0][1]['msg'] == 'No open trade found.'
# Create some test data
freqtradebot.enter_positions()
msg_mock.reset_mock()
# /forceexit
telegram._force_exit(update=update, context=context)
keyboard = msg_mock.call_args_list[0][1]['keyboard']
# 4 pairs + cancel
assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5
assert keyboard[-1][0].text == "Cancel"
assert keyboard[1][0].callback_data == 'force_exit__2 '
update = MagicMock()
update.callback_query = MagicMock()
update.callback_query.data = keyboard[1][0].callback_data
telegram._force_exit_inline(update, None)
assert update.callback_query.answer.call_count == 1
assert update.callback_query.edit_message_text.call_count == 1
assert femock.call_count == 1
assert femock.call_args_list[0][0][0] == '2'
# Retry exiting - but cancel instead
update.callback_query.reset_mock()
telegram._force_exit(update=update, context=context)
# Use cancel button
update.callback_query.data = keyboard[-1][0].callback_data
telegram._force_exit_inline(update, None)
query = update.callback_query
assert query.answer.call_count == 1
assert query.edit_message_text.call_count == 1
assert query.edit_message_text.call_args_list[-1][1]['text'] == "Force exit canceled."
def test_force_enter_handle(default_conf, update, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
fbuy_mock = MagicMock(return_value=None) fbuy_mock = MagicMock(return_value=None)
@ -1243,7 +1288,7 @@ def test_forceenter_handle(default_conf, update, mocker) -> None:
# /forcelong ETH/BTC # /forcelong ETH/BTC
context = MagicMock() context = MagicMock()
context.args = ["ETH/BTC"] context.args = ["ETH/BTC"]
telegram._forceenter(update=update, context=context, order_side=SignalDirection.LONG) telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG)
assert fbuy_mock.call_count == 1 assert fbuy_mock.call_count == 1
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC'
@ -1256,7 +1301,7 @@ def test_forceenter_handle(default_conf, update, mocker) -> None:
# /forcelong ETH/BTC 0.055 # /forcelong ETH/BTC 0.055
context = MagicMock() context = MagicMock()
context.args = ["ETH/BTC", "0.055"] context.args = ["ETH/BTC", "0.055"]
telegram._forceenter(update=update, context=context, order_side=SignalDirection.LONG) telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG)
assert fbuy_mock.call_count == 1 assert fbuy_mock.call_count == 1
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC'
@ -1264,20 +1309,20 @@ def test_forceenter_handle(default_conf, update, mocker) -> None:
assert fbuy_mock.call_args_list[0][0][1] == 0.055 assert fbuy_mock.call_args_list[0][0][1] == 0.055
def test_forceenter_handle_exception(default_conf, update, mocker) -> None: def test_force_enter_handle_exception(default_conf, update, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot) patch_get_signal(freqtradebot)
update.message.text = '/forcebuy ETH/Nonepair' update.message.text = '/forcebuy ETH/Nonepair'
telegram._forceenter(update=update, context=MagicMock(), order_side=SignalDirection.LONG) telegram._force_enter(update=update, context=MagicMock(), order_side=SignalDirection.LONG)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert msg_mock.call_args_list[0][0][0] == 'Forceentry not enabled.' assert msg_mock.call_args_list[0][0][0] == 'Force_entry not enabled.'
def test_forceenter_no_pair(default_conf, update, mocker) -> None: def test_force_enter_no_pair(default_conf, update, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
fbuy_mock = MagicMock(return_value=None) fbuy_mock = MagicMock(return_value=None)
@ -1289,7 +1334,7 @@ def test_forceenter_no_pair(default_conf, update, mocker) -> None:
context = MagicMock() context = MagicMock()
context.args = [] context.args = []
telegram._forceenter(update=update, context=context, order_side=SignalDirection.LONG) telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG)
assert fbuy_mock.call_count == 0 assert fbuy_mock.call_count == 0
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -1301,7 +1346,7 @@ def test_forceenter_no_pair(default_conf, update, mocker) -> None:
update = MagicMock() update = MagicMock()
update.callback_query = MagicMock() update.callback_query = MagicMock()
update.callback_query.data = 'XRP/USDT_||_long' update.callback_query.data = 'XRP/USDT_||_long'
telegram._forceenter_inline(update, None) telegram._force_enter_inline(update, None)
assert fbuy_mock.call_count == 1 assert fbuy_mock.call_count == 1
@ -1771,10 +1816,10 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
@pytest.mark.parametrize('message_type,enter,enter_signal,leverage', [ @pytest.mark.parametrize('message_type,enter,enter_signal,leverage', [
(RPCMessageType.BUY, 'Long', 'long_signal_01', None), (RPCMessageType.ENTRY, 'Long', 'long_signal_01', None),
(RPCMessageType.BUY, 'Long', 'long_signal_01', 1.0), (RPCMessageType.ENTRY, 'Long', 'long_signal_01', 1.0),
(RPCMessageType.BUY, 'Long', 'long_signal_01', 5.0), (RPCMessageType.ENTRY, 'Long', 'long_signal_01', 5.0),
(RPCMessageType.SHORT, 'Short', 'short_signal_01', 2.0)]) (RPCMessageType.ENTRY, 'Short', 'short_signal_01', 2.0)])
def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type, def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
enter, enter_signal, leverage) -> None: enter, enter_signal, leverage) -> None:
@ -1787,6 +1832,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
'leverage': leverage, 'leverage': leverage,
'limit': 1.099e-05, 'limit': 1.099e-05,
'order_type': 'limit', 'order_type': 'limit',
'direction': enter,
'stake_amount': 0.01465333, 'stake_amount': 0.01465333,
'stake_amount_fiat': 0.0, 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
@ -1827,8 +1873,8 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
@pytest.mark.parametrize('message_type,enter_signal', [ @pytest.mark.parametrize('message_type,enter_signal', [
(RPCMessageType.BUY_CANCEL, 'long_signal_01'), (RPCMessageType.ENTRY_CANCEL, 'long_signal_01'),
(RPCMessageType.SHORT_CANCEL, 'short_signal_01')]) (RPCMessageType.ENTRY_CANCEL, 'short_signal_01')])
def test_send_msg_buy_cancel_notification(default_conf, mocker, message_type, enter_signal) -> None: def test_send_msg_buy_cancel_notification(default_conf, mocker, message_type, enter_signal) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
@ -1875,14 +1921,14 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) ->
@pytest.mark.parametrize('message_type,entered,enter_signal,leverage', [ @pytest.mark.parametrize('message_type,entered,enter_signal,leverage', [
(RPCMessageType.BUY_FILL, 'Longed', 'long_signal_01', 1.0), (RPCMessageType.ENTRY_FILL, 'Long', 'long_signal_01', 1.0),
(RPCMessageType.BUY_FILL, 'Longed', 'long_signal_02', 2.0), (RPCMessageType.ENTRY_FILL, 'Long', 'long_signal_02', 2.0),
(RPCMessageType.SHORT_FILL, 'Shorted', 'short_signal_01', 2.0), (RPCMessageType.ENTRY_FILL, 'Short', 'short_signal_01', 2.0),
]) ])
def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, entered, def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, entered,
enter_signal, leverage) -> None: enter_signal, leverage) -> None:
default_conf['telegram']['notification_settings']['buy_fill'] = 'on' default_conf['telegram']['notification_settings']['entry_fill'] = 'on'
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({ telegram.send_msg({
@ -1893,6 +1939,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, ente
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': leverage, 'leverage': leverage,
'stake_amount': 0.01465333, 'stake_amount': 0.01465333,
'direction': entered,
# 'stake_amount_fiat': 0.0, # 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
@ -1902,7 +1949,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, ente
}) })
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage != 1.0 else '' leverage_text = f'*Leverage:* `{leverage}`\n' if leverage != 1.0 else ''
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
f'\N{CHECK MARK} *Binance:* {entered} ETH/BTC (#1)\n' f'\N{CHECK MARK} *Binance:* {entered}ed ETH/BTC (#1)\n'
f'*Enter Tag:* `{enter_signal}`\n' f'*Enter Tag:* `{enter_signal}`\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
f"{leverage_text}" f"{leverage_text}"
@ -1918,7 +1965,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
old_convamount = telegram._rpc._fiat_converter.convert_amount old_convamount = telegram._rpc._fiat_converter.convert_amount
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
@ -1954,7 +2001,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
msg_mock.reset_mock() msg_mock.reset_mock()
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
@ -1996,7 +2043,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
old_convamount = telegram._rpc._fiat_converter.convert_amount old_convamount = telegram._rpc._fiat_converter.convert_amount
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL, 'type': RPCMessageType.EXIT_CANCEL,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
@ -2008,7 +2055,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
msg_mock.reset_mock() msg_mock.reset_mock()
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL, 'type': RPCMessageType.EXIT_CANCEL,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
@ -2028,11 +2075,11 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
def test_send_msg_sell_fill_notification(default_conf, mocker, direction, def test_send_msg_sell_fill_notification(default_conf, mocker, direction,
enter_signal, leverage) -> None: enter_signal, leverage) -> None:
default_conf['telegram']['notification_settings']['sell_fill'] = 'on' default_conf['telegram']['notification_settings']['exit_fill'] = 'on'
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL_FILL, 'type': RPCMessageType.EXIT_FILL,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
@ -2105,9 +2152,9 @@ def test_send_msg_unknown_type(default_conf, mocker) -> None:
@pytest.mark.parametrize('message_type,enter,enter_signal,leverage', [ @pytest.mark.parametrize('message_type,enter,enter_signal,leverage', [
(RPCMessageType.BUY, 'Long', 'long_signal_01', None), (RPCMessageType.ENTRY, 'Long', 'long_signal_01', None),
(RPCMessageType.BUY, 'Long', 'long_signal_01', 2.0), (RPCMessageType.ENTRY, 'Long', 'long_signal_01', 2.0),
(RPCMessageType.SHORT, 'Short', 'short_signal_01', 2.0)]) (RPCMessageType.ENTRY, 'Short', 'short_signal_01', 2.0)])
def test_send_msg_buy_notification_no_fiat( def test_send_msg_buy_notification_no_fiat(
default_conf, mocker, message_type, enter, enter_signal, leverage) -> None: default_conf, mocker, message_type, enter, enter_signal, leverage) -> None:
del default_conf['fiat_display_currency'] del default_conf['fiat_display_currency']
@ -2122,6 +2169,7 @@ def test_send_msg_buy_notification_no_fiat(
'leverage': leverage, 'leverage': leverage,
'limit': 1.099e-05, 'limit': 1.099e-05,
'order_type': 'limit', 'order_type': 'limit',
'direction': enter,
'stake_amount': 0.01465333, 'stake_amount': 0.01465333,
'stake_amount_fiat': 0.0, 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
@ -2155,7 +2203,7 @@ def test_send_msg_sell_notification_no_fiat(
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',

View File

@ -15,38 +15,38 @@ def get_webhook_dict() -> dict:
return { return {
"enabled": True, "enabled": True,
"url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/", "url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
"webhookbuy": { "webhookentry": {
"value1": "Buying {pair}", "value1": "Buying {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}", "value3": "{stake_amount:8f} {stake_currency}",
"value4": "leverage {leverage:.1f}", "value4": "leverage {leverage:.1f}",
"value5": "direction {direction}" "value5": "direction {direction}"
}, },
"webhookbuycancel": { "webhookentrycancel": {
"value1": "Cancelling Open Buy Order for {pair}", "value1": "Cancelling Open Buy Order for {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}", "value3": "{stake_amount:8f} {stake_currency}",
"value4": "leverage {leverage:.1f}", "value4": "leverage {leverage:.1f}",
"value5": "direction {direction}" "value5": "direction {direction}"
}, },
"webhookbuyfill": { "webhookentryfill": {
"value1": "Buy Order for {pair} filled", "value1": "Buy Order for {pair} filled",
"value2": "at {open_rate:8f}", "value2": "at {open_rate:8f}",
"value3": "{stake_amount:8f} {stake_currency}", "value3": "{stake_amount:8f} {stake_currency}",
"value4": "leverage {leverage:.1f}", "value4": "leverage {leverage:.1f}",
"value5": "direction {direction}" "value5": "direction {direction}"
}, },
"webhooksell": { "webhookexit": {
"value1": "Selling {pair}", "value1": "Selling {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
}, },
"webhooksellcancel": { "webhookexitcancel": {
"value1": "Cancelling Open Sell Order for {pair}", "value1": "Cancelling Open Sell Order for {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
}, },
"webhooksellfill": { "webhookexitfill": {
"value1": "Sell Order for {pair} filled", "value1": "Sell Order for {pair} filled",
"value2": "at {close_rate:8f}", "value2": "at {close_rate:8f}",
"value3": "" "value3": ""
@ -74,7 +74,7 @@ def test_send_msg_webhook(default_conf, mocker):
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
msg = { msg = {
'type': RPCMessageType.BUY, 'type': RPCMessageType.ENTRY,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': 1.0, 'leverage': 1.0,
@ -88,20 +88,20 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuy"]["value1"].format(**msg)) default_conf["webhook"]["webhookentry"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuy"]["value2"].format(**msg)) default_conf["webhook"]["webhookentry"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg)) default_conf["webhook"]["webhookentry"]["value3"].format(**msg))
assert (msg_mock.call_args[0][0]["value4"] == assert (msg_mock.call_args[0][0]["value4"] ==
default_conf["webhook"]["webhookbuy"]["value4"].format(**msg)) default_conf["webhook"]["webhookentry"]["value4"].format(**msg))
assert (msg_mock.call_args[0][0]["value5"] == assert (msg_mock.call_args[0][0]["value5"] ==
default_conf["webhook"]["webhookbuy"]["value5"].format(**msg)) default_conf["webhook"]["webhookentry"]["value5"].format(**msg))
# Test short # Test short
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.SHORT, 'type': RPCMessageType.ENTRY,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': 2.0, 'leverage': 2.0,
@ -115,20 +115,20 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuy"]["value1"].format(**msg)) default_conf["webhook"]["webhookentry"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuy"]["value2"].format(**msg)) default_conf["webhook"]["webhookentry"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg)) default_conf["webhook"]["webhookentry"]["value3"].format(**msg))
assert (msg_mock.call_args[0][0]["value4"] == assert (msg_mock.call_args[0][0]["value4"] ==
default_conf["webhook"]["webhookbuy"]["value4"].format(**msg)) default_conf["webhook"]["webhookentry"]["value4"].format(**msg))
assert (msg_mock.call_args[0][0]["value5"] == assert (msg_mock.call_args[0][0]["value5"] ==
default_conf["webhook"]["webhookbuy"]["value5"].format(**msg)) default_conf["webhook"]["webhookentry"]["value5"].format(**msg))
# Test buy cancel # Test buy cancel
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.BUY_CANCEL, 'type': RPCMessageType.ENTRY_CANCEL,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': 1.0, 'leverage': 1.0,
@ -142,16 +142,16 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuycancel"]["value1"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value3"].format(**msg))
# Test short cancel # Test short cancel
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.SHORT_CANCEL, 'type': RPCMessageType.ENTRY_CANCEL,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': 2.0, 'leverage': 2.0,
@ -165,20 +165,20 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuycancel"]["value1"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value3"].format(**msg))
assert (msg_mock.call_args[0][0]["value4"] == assert (msg_mock.call_args[0][0]["value4"] ==
default_conf["webhook"]["webhookbuycancel"]["value4"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value4"].format(**msg))
assert (msg_mock.call_args[0][0]["value5"] == assert (msg_mock.call_args[0][0]["value5"] ==
default_conf["webhook"]["webhookbuycancel"]["value5"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value5"].format(**msg))
# Test buy fill # Test buy fill
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.BUY_FILL, 'type': RPCMessageType.ENTRY_FILL,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': 1.0, 'leverage': 1.0,
@ -192,20 +192,20 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuyfill"]["value1"].format(**msg)) default_conf["webhook"]["webhookentryfill"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuyfill"]["value2"].format(**msg)) default_conf["webhook"]["webhookentryfill"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuyfill"]["value3"].format(**msg)) default_conf["webhook"]["webhookentryfill"]["value3"].format(**msg))
assert (msg_mock.call_args[0][0]["value4"] == assert (msg_mock.call_args[0][0]["value4"] ==
default_conf["webhook"]["webhookbuycancel"]["value4"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value4"].format(**msg))
assert (msg_mock.call_args[0][0]["value5"] == assert (msg_mock.call_args[0][0]["value5"] ==
default_conf["webhook"]["webhookbuycancel"]["value5"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value5"].format(**msg))
# Test short fill # Test short fill
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.SHORT_FILL, 'type': RPCMessageType.ENTRY_FILL,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': 2.0, 'leverage': 2.0,
@ -219,20 +219,20 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuyfill"]["value1"].format(**msg)) default_conf["webhook"]["webhookentryfill"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuyfill"]["value2"].format(**msg)) default_conf["webhook"]["webhookentryfill"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuyfill"]["value3"].format(**msg)) default_conf["webhook"]["webhookentryfill"]["value3"].format(**msg))
assert (msg_mock.call_args[0][0]["value4"] == assert (msg_mock.call_args[0][0]["value4"] ==
default_conf["webhook"]["webhookbuycancel"]["value4"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value4"].format(**msg))
assert (msg_mock.call_args[0][0]["value5"] == assert (msg_mock.call_args[0][0]["value5"] ==
default_conf["webhook"]["webhookbuycancel"]["value5"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value5"].format(**msg))
# Test sell # Test sell
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': "profit", 'gain': "profit",
@ -249,15 +249,15 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhooksell"]["value1"].format(**msg)) default_conf["webhook"]["webhookexit"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhooksell"]["value2"].format(**msg)) default_conf["webhook"]["webhookexit"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksell"]["value3"].format(**msg)) default_conf["webhook"]["webhookexit"]["value3"].format(**msg))
# Test sell cancel # Test sell cancel
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.SELL_CANCEL, 'type': RPCMessageType.EXIT_CANCEL,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': "profit", 'gain': "profit",
@ -274,15 +274,15 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhooksellcancel"]["value1"].format(**msg)) default_conf["webhook"]["webhookexitcancel"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhooksellcancel"]["value2"].format(**msg)) default_conf["webhook"]["webhookexitcancel"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg)) default_conf["webhook"]["webhookexitcancel"]["value3"].format(**msg))
# Test Sell fill # Test Sell fill
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.SELL_FILL, 'type': RPCMessageType.EXIT_FILL,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': "profit", 'gain': "profit",
@ -299,11 +299,11 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhooksellfill"]["value1"].format(**msg)) default_conf["webhook"]["webhookexitfill"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhooksellfill"]["value2"].format(**msg)) default_conf["webhook"]["webhookexitfill"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksellfill"]["value3"].format(**msg)) default_conf["webhook"]["webhookexitfill"]["value3"].format(**msg))
for msgtype in [RPCMessageType.STATUS, for msgtype in [RPCMessageType.STATUS,
RPCMessageType.WARNING, RPCMessageType.WARNING,
@ -327,20 +327,20 @@ def test_send_msg_webhook(default_conf, mocker):
def test_exception_send_msg(default_conf, mocker, caplog): def test_exception_send_msg(default_conf, mocker, caplog):
default_conf["webhook"] = get_webhook_dict() default_conf["webhook"] = get_webhook_dict()
del default_conf["webhook"]["webhookbuy"] del default_conf["webhook"]["webhookentry"]
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
webhook.send_msg({'type': RPCMessageType.BUY}) webhook.send_msg({'type': RPCMessageType.ENTRY})
assert log_has(f"Message type '{RPCMessageType.BUY}' not configured for webhooks", assert log_has(f"Message type '{RPCMessageType.ENTRY}' not configured for webhooks",
caplog) caplog)
default_conf["webhook"] = get_webhook_dict() default_conf["webhook"] = get_webhook_dict()
default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}" default_conf["webhook"]["webhookentry"]["value1"] = "{DEADBEEF:8f}"
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
msg = { msg = {
'type': RPCMessageType.BUY, 'type': RPCMessageType.ENTRY,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'limit': 0.005, 'limit': 0.005,

View File

@ -50,6 +50,8 @@ class StrategyTestV2(IStrategy):
'entry': 'gtc', 'entry': 'gtc',
'exit': 'gtc', 'exit': 'gtc',
} }
# Test legacy use_sell_signal definition
use_sell_signal = False
# By default this strategy does not use Position Adjustments # By default this strategy does not use Position Adjustments
position_adjustment_enable = False position_adjustment_enable = False

View File

@ -183,7 +183,7 @@ class StrategyTestV3(IStrategy):
current_profit: float, min_stake: float, max_stake: float, **kwargs): current_profit: float, min_stake: float, max_stake: float, **kwargs):
if current_profit < -0.0075: if current_profit < -0.0075:
orders = trade.select_filled_orders(trade.enter_side) orders = trade.select_filled_orders(trade.entry_side)
return round(orders[0].cost, 0) return round(orders[0].cost, 0)
return None return None

View File

@ -503,15 +503,15 @@ def test_custom_exit(default_conf, fee, caplog) -> None:
enter=False, exit_=False, enter=False, exit_=False,
low=None, high=None) low=None, high=None)
assert res.exit_flag is True assert res.exit_flag is True
assert res.exit_type == ExitType.CUSTOM_SELL assert res.exit_type == ExitType.CUSTOM_EXIT
assert res.exit_reason == 'custom_sell' assert res.exit_reason == 'custom_exit'
strategy.custom_exit = MagicMock(return_value='hello world') strategy.custom_exit = MagicMock(return_value='hello world')
res = strategy.should_exit(trade, 1, now, res = strategy.should_exit(trade, 1, now,
enter=False, exit_=False, enter=False, exit_=False,
low=None, high=None) low=None, high=None)
assert res.exit_type == ExitType.CUSTOM_SELL assert res.exit_type == ExitType.CUSTOM_EXIT
assert res.exit_flag is True assert res.exit_flag is True
assert res.exit_reason == 'hello world' assert res.exit_reason == 'hello world'
@ -520,10 +520,10 @@ def test_custom_exit(default_conf, fee, caplog) -> None:
res = strategy.should_exit(trade, 1, now, res = strategy.should_exit(trade, 1, now,
enter=False, exit_=False, enter=False, exit_=False,
low=None, high=None) low=None, high=None)
assert res.exit_type == ExitType.CUSTOM_SELL assert res.exit_type == ExitType.CUSTOM_EXIT
assert res.exit_flag is True assert res.exit_flag is True
assert res.exit_reason == 'h' * 64 assert res.exit_reason == 'h' * 64
assert log_has_re('Custom sell reason returned from custom_exit is too long.*', caplog) assert log_has_re('Custom exit reason returned from custom_exit is too long.*', caplog)
@pytest.mark.parametrize('side', TRADE_SIDES) @pytest.mark.parametrize('side', TRADE_SIDES)

View File

@ -143,16 +143,6 @@ def test_strategy_can_short(caplog, default_conf):
assert isinstance(strat, IStrategy) assert isinstance(strat, IStrategy)
def test_strategy_implements_populate_entry(caplog, default_conf):
caplog.set_level(logging.INFO)
default_conf.update({
'strategy': "StrategyTestV2",
})
default_conf['trading_mode'] = 'futures'
with pytest.raises(OperationalException, match="`populate_entry_trend` must be implemented."):
StrategyResolver.load_strategy(default_conf)
def test_strategy_override_minimal_roi(caplog, default_conf): def test_strategy_override_minimal_roi(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
@ -310,50 +300,50 @@ def test_strategy_override_order_tif(caplog, default_conf):
StrategyResolver.load_strategy(default_conf) StrategyResolver.load_strategy(default_conf)
def test_strategy_override_use_sell_signal(caplog, default_conf): def test_strategy_override_use_exit_signal(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': CURRENT_TEST_STRATEGY, 'strategy': CURRENT_TEST_STRATEGY,
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.use_sell_signal assert strategy.use_exit_signal
assert isinstance(strategy.use_sell_signal, bool) assert isinstance(strategy.use_exit_signal, bool)
# must be inserted to configuration # must be inserted to configuration
assert 'use_sell_signal' in default_conf assert 'use_exit_signal' in default_conf
assert default_conf['use_sell_signal'] assert default_conf['use_exit_signal']
default_conf.update({ default_conf.update({
'strategy': CURRENT_TEST_STRATEGY, 'strategy': CURRENT_TEST_STRATEGY,
'use_sell_signal': False, 'use_exit_signal': False,
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert not strategy.use_sell_signal assert not strategy.use_exit_signal
assert isinstance(strategy.use_sell_signal, bool) assert isinstance(strategy.use_exit_signal, bool)
assert log_has("Override strategy 'use_sell_signal' with value in config file: False.", caplog) assert log_has("Override strategy 'use_exit_signal' with value in config file: False.", caplog)
def test_strategy_override_use_sell_profit_only(caplog, default_conf): def test_strategy_override_use_exit_profit_only(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': CURRENT_TEST_STRATEGY, 'strategy': CURRENT_TEST_STRATEGY,
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert not strategy.sell_profit_only assert not strategy.exit_profit_only
assert isinstance(strategy.sell_profit_only, bool) assert isinstance(strategy.exit_profit_only, bool)
# must be inserted to configuration # must be inserted to configuration
assert 'sell_profit_only' in default_conf assert 'exit_profit_only' in default_conf
assert not default_conf['sell_profit_only'] assert not default_conf['exit_profit_only']
default_conf.update({ default_conf.update({
'strategy': CURRENT_TEST_STRATEGY, 'strategy': CURRENT_TEST_STRATEGY,
'sell_profit_only': True, 'exit_profit_only': True,
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.sell_profit_only assert strategy.exit_profit_only
assert isinstance(strategy.sell_profit_only, bool) assert isinstance(strategy.exit_profit_only, bool)
assert log_has("Override strategy 'sell_profit_only' with value in config file: True.", caplog) assert log_has("Override strategy 'exit_profit_only' with value in config file: True.", caplog)
@pytest.mark.filterwarnings("ignore:deprecated") @pytest.mark.filterwarnings("ignore:deprecated")
@ -391,7 +381,22 @@ def test_deprecate_populate_indicators(result, default_conf):
@pytest.mark.filterwarnings("ignore:deprecated") @pytest.mark.filterwarnings("ignore:deprecated")
def test_missing_implements(default_conf): def test_missing_implements(default_conf, caplog):
default_location = Path(__file__).parent / "strats"
default_conf.update({'strategy': 'StrategyTestV2',
'strategy_path': default_location})
StrategyResolver.load_strategy(default_conf)
log_has_re(r"DEPRECATED: .*use_sell_signal.*use_exit_signal.", caplog)
default_conf['trading_mode'] = 'futures'
with pytest.raises(OperationalException,
match=r"DEPRECATED: .*use_sell_signal.*use_exit_signal."):
StrategyResolver.load_strategy(default_conf)
default_conf['trading_mode'] = 'spot'
default_location = Path(__file__).parent / "strats/broken_strats" default_location = Path(__file__).parent / "strats/broken_strats"
default_conf.update({'strategy': 'TestStrategyNoImplements', default_conf.update({'strategy': 'TestStrategyNoImplements',
'strategy_path': default_location}) 'strategy_path': default_location})

View File

@ -18,7 +18,8 @@ from freqtrade.configuration.deprecated_settings import (check_conflicting_setti
process_removed_setting, process_removed_setting,
process_temporary_deprecated_settings) process_temporary_deprecated_settings)
from freqtrade.configuration.environment_vars import flat_vars_to_nested_dict from freqtrade.configuration.environment_vars import flat_vars_to_nested_dict
from freqtrade.configuration.load_config import load_config_file, load_file, log_config_error_range from freqtrade.configuration.load_config import (load_config_file, load_file, load_from_files,
log_config_error_range)
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@ -160,7 +161,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
configsmock = MagicMock(side_effect=config_files) configsmock = MagicMock(side_effect=config_files)
mocker.patch( mocker.patch(
'freqtrade.configuration.configuration.load_config_file', 'freqtrade.configuration.load_config.load_config_file',
configsmock configsmock
) )
@ -191,7 +192,7 @@ def test_from_config(default_conf, mocker, caplog) -> None:
mocker.patch('freqtrade.configuration.configuration.create_datadir', lambda c, x: x) mocker.patch('freqtrade.configuration.configuration.create_datadir', lambda c, x: x)
configsmock = MagicMock(side_effect=config_files) configsmock = MagicMock(side_effect=config_files)
mocker.patch('freqtrade.configuration.configuration.load_config_file', configsmock) mocker.patch('freqtrade.configuration.load_config.load_config_file', configsmock)
validated_conf = Configuration.from_files(['test_conf.json', 'test2_conf.json']) validated_conf = Configuration.from_files(['test_conf.json', 'test2_conf.json'])
@ -206,6 +207,33 @@ def test_from_config(default_conf, mocker, caplog) -> None:
assert isinstance(validated_conf['user_data_dir'], Path) assert isinstance(validated_conf['user_data_dir'], Path)
def test_from_recursive_files(testdatadir) -> None:
files = testdatadir / "testconfigs/testconfig.json"
conf = Configuration.from_files([files])
assert conf
# Exchange comes from "the first config"
assert conf['exchange']
# Pricing comes from the 2nd config
assert conf['entry_pricing']
assert conf['entry_pricing']['price_side'] == "same"
assert conf['exit_pricing']
# The other key comes from pricing2, which is imported by pricing.json.
# pricing.json is a level higher, therefore wins.
assert conf['exit_pricing']['price_side'] == "same"
assert len(conf['config_files']) == 4
assert 'testconfig.json' in conf['config_files'][0]
assert 'test_pricing_conf.json' in conf['config_files'][1]
assert 'test_base_config.json' in conf['config_files'][2]
assert 'test_pricing2_conf.json' in conf['config_files'][3]
files = testdatadir / "testconfigs/recursive.json"
with pytest.raises(OperationalException, match="Config loop detected."):
load_from_files([files])
def test_print_config(default_conf, mocker, caplog) -> None: def test_print_config(default_conf, mocker, caplog) -> None:
conf1 = deepcopy(default_conf) conf1 = deepcopy(default_conf)
# Delete non-json elements from default_conf # Delete non-json elements from default_conf
@ -214,7 +242,7 @@ def test_print_config(default_conf, mocker, caplog) -> None:
configsmock = MagicMock(side_effect=config_files) configsmock = MagicMock(side_effect=config_files)
mocker.patch('freqtrade.configuration.configuration.create_datadir', lambda c, x: x) mocker.patch('freqtrade.configuration.configuration.create_datadir', lambda c, x: x)
mocker.patch('freqtrade.configuration.configuration.load_config_file', configsmock) mocker.patch('freqtrade.configuration.configuration.load_from_files', configsmock)
validated_conf = Configuration.from_files(['test_conf.json']) validated_conf = Configuration.from_files(['test_conf.json'])
@ -772,15 +800,15 @@ def test_set_logfile(default_conf, mocker, tmpdir):
def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
default_conf['forcebuy_enable'] = True default_conf['force_entry_enable'] = True
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = Arguments(['trade']).get_parsed_arg() args = Arguments(['trade']).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
assert validated_conf.get('forcebuy_enable') assert validated_conf.get('force_entry_enable')
assert log_has('`forcebuy` RPC message enabled.', caplog) assert log_has('`force_entry_enable` RPC message enabled.', caplog)
def test_validate_default_conf(default_conf) -> None: def test_validate_default_conf(default_conf) -> None:
@ -868,15 +896,15 @@ def test_validate_tsl(default_conf):
def test_validate_edge2(edge_conf): def test_validate_edge2(edge_conf):
edge_conf.update({ edge_conf.update({
"use_sell_signal": True, "use_exit_signal": True,
}) })
# Passes test # Passes test
validate_config_consistency(edge_conf) validate_config_consistency(edge_conf)
edge_conf.update({ edge_conf.update({
"use_sell_signal": False, "use_exit_signal": False,
}) })
with pytest.raises(OperationalException, match="Edge requires `use_sell_signal` to be True, " with pytest.raises(OperationalException, match="Edge requires `use_exit_signal` to be True, "
"otherwise no sells will happen."): "otherwise no sells will happen."):
validate_config_consistency(edge_conf) validate_config_consistency(edge_conf)
@ -977,7 +1005,7 @@ def test__validate_order_types(default_conf, caplog) -> None:
assert log_has_re(r"DEPRECATED: Using 'buy' and 'sell' for order_types is.*", caplog) assert log_has_re(r"DEPRECATED: Using 'buy' and 'sell' for order_types is.*", caplog)
assert conf['order_types']['entry'] == 'limit' assert conf['order_types']['entry'] == 'limit'
assert conf['order_types']['exit'] == 'market' assert conf['order_types']['exit'] == 'market'
assert conf['order_types']['forceentry'] == 'limit' assert conf['order_types']['force_entry'] == 'limit'
assert 'buy' not in conf['order_types'] assert 'buy' not in conf['order_types']
assert 'sell' not in conf['order_types'] assert 'sell' not in conf['order_types']
assert 'forcebuy' not in conf['order_types'] assert 'forcebuy' not in conf['order_types']
@ -1238,14 +1266,8 @@ def test_pairlist_resolving_fallback(mocker):
@pytest.mark.parametrize("setting", [ @pytest.mark.parametrize("setting", [
("ask_strategy", "use_sell_signal", True, ("webhook", "webhookbuy", 'testWEbhook',
None, "use_sell_signal", False), "webhook", "webhookentry", 'testWEbhook'),
("ask_strategy", "sell_profit_only", True,
None, "sell_profit_only", False),
("ask_strategy", "sell_profit_offset", 0.1,
None, "sell_profit_offset", 0.01),
("ask_strategy", "ignore_roi_if_buy_signal", True,
None, "ignore_roi_if_buy_signal", False),
("ask_strategy", "ignore_buying_expired_candle_after", 5, ("ask_strategy", "ignore_buying_expired_candle_after", 5,
None, "ignore_buying_expired_candle_after", 6), None, "ignore_buying_expired_candle_after", 6),
]) ])

View File

@ -25,7 +25,7 @@ from freqtrade.worker import Worker
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker,
log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal,
patch_wallet, patch_whitelist) patch_wallet, patch_whitelist)
from tests.conftest_trades import (MOCK_TRADE_COUNT, enter_side, exit_side, mock_order_1, from tests.conftest_trades import (MOCK_TRADE_COUNT, entry_side, exit_side, mock_order_1,
mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell,
mock_order_4, mock_order_5_stoploss, mock_order_6_sell) mock_order_4, mock_order_5_stoploss, mock_order_6_sell)
@ -303,7 +303,7 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_order,
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object( oobj = Order.parse_from_ccxt_object(
limit_order[enter_side(is_short)], 'ADA/USDT', enter_side(is_short)) limit_order[entry_side(is_short)], 'ADA/USDT', entry_side(is_short))
trade.update_trade(oobj) trade.update_trade(oobj)
assert trade.open_rate == open_rate assert trade.open_rate == open_rate
@ -341,7 +341,7 @@ def test_create_trade_minimal_amount(
) -> None: ) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
enter_mock = MagicMock(return_value=limit_order_open[enter_side(is_short)]) enter_mock = MagicMock(return_value=limit_order_open[entry_side(is_short)])
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker_usdt, fetch_ticker=ticker_usdt,
@ -537,8 +537,8 @@ def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, lim
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker_usdt, fetch_ticker=ticker_usdt,
create_order=MagicMock(return_value=limit_order_open[enter_side(is_short)]), create_order=MagicMock(return_value=limit_order_open[entry_side(is_short)]),
fetch_order=MagicMock(return_value=limit_order[enter_side(is_short)]), fetch_order=MagicMock(return_value=limit_order[entry_side(is_short)]),
get_fee=fee, get_fee=fee,
) )
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
@ -751,8 +751,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
(10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207 (10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207
""" """
# TODO: Split this test into multiple tests to improve readability # TODO: Split this test into multiple tests to improve readability
open_order = limit_order_open[enter_side(is_short)] open_order = limit_order_open[entry_side(is_short)]
order = limit_order[enter_side(is_short)] order = limit_order[entry_side(is_short)]
default_conf_usdt['trading_mode'] = trading_mode default_conf_usdt['trading_mode'] = trading_mode
default_conf_usdt['liquidation_buffer'] = liq_buffer default_conf_usdt['liquidation_buffer'] = liq_buffer
leverage = 1.0 if trading_mode == 'spot' else 5.0 leverage = 1.0 if trading_mode == 'spot' else 5.0
@ -975,7 +975,7 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order
'ask': 2.2, 'ask': 2.2,
'last': 1.9 'last': 1.9
}), }),
create_order=MagicMock(return_value=limit_order[enter_side(is_short)]), create_order=MagicMock(return_value=limit_order[entry_side(is_short)]),
get_rate=MagicMock(return_value=0.11), get_rate=MagicMock(return_value=0.11),
get_min_pair_stake_amount=MagicMock(return_value=1), get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee, get_fee=fee,
@ -986,11 +986,11 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order
freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError) freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError)
assert freqtrade.execute_entry(pair, stake_amount) assert freqtrade.execute_entry(pair, stake_amount)
limit_order[enter_side(is_short)]['id'] = '222' limit_order[entry_side(is_short)]['id'] = '222'
freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception) freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception)
assert freqtrade.execute_entry(pair, stake_amount) assert freqtrade.execute_entry(pair, stake_amount)
limit_order[enter_side(is_short)]['id'] = '2223' limit_order[entry_side(is_short)]['id'] = '2223'
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
assert freqtrade.execute_entry(pair, stake_amount) assert freqtrade.execute_entry(pair, stake_amount)
@ -1010,7 +1010,7 @@ def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order,
'ask': 2.2, 'ask': 2.2,
'last': 1.9 'last': 1.9
}), }),
create_order=MagicMock(return_value=limit_order[enter_side(is_short)]), create_order=MagicMock(return_value=limit_order[entry_side(is_short)]),
get_rate=MagicMock(return_value=0.11), get_rate=MagicMock(return_value=0.11),
# Minimum stake-amount is ~5$ # Minimum stake-amount is ~5$
get_maintenance_ratio_and_amt=MagicMock(return_value=(0.0, 0.0)), get_maintenance_ratio_and_amt=MagicMock(return_value=(0.0, 0.0)),
@ -1032,7 +1032,7 @@ def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order,
def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None: def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
order = limit_order[enter_side(is_short)] order = limit_order[entry_side(is_short)]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
@ -1062,7 +1062,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho
def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_short, def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_short,
limit_order) -> None: limit_order) -> None:
stoploss = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
enter_order = limit_order[enter_side(is_short)] enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)] exit_order = limit_order[exit_side(is_short)]
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
@ -1210,14 +1210,14 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
assert trade.is_open is False assert trade.is_open is False
assert trade.exit_reason == str(ExitType.EMERGENCY_SELL) assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT)
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short, def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short,
limit_order) -> None: limit_order) -> None:
# Sixth case: stoploss order was cancelled but couldn't create new one # Sixth case: stoploss order was cancelled but couldn't create new one
enter_order = limit_order[enter_side(is_short)] enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)] exit_order = limit_order[exit_side(is_short)]
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
@ -1260,7 +1260,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
def test_create_stoploss_order_invalid_order( def test_create_stoploss_order_invalid_order(
mocker, default_conf_usdt, caplog, fee, is_short, limit_order, limit_order_open mocker, default_conf_usdt, caplog, fee, is_short, limit_order, limit_order_open
): ):
open_order = limit_order_open[enter_side(is_short)] open_order = limit_order_open[entry_side(is_short)]
order = limit_order[exit_side(is_short)] order = limit_order[exit_side(is_short)]
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
@ -1293,7 +1293,7 @@ def test_create_stoploss_order_invalid_order(
caplog.clear() caplog.clear()
freqtrade.create_stoploss_order(trade, 200) freqtrade.create_stoploss_order(trade, 200)
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
assert trade.exit_reason == ExitType.EMERGENCY_SELL.value assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value
assert log_has("Unable to place a stoploss order on exchange. ", caplog) assert log_has("Unable to place a stoploss order on exchange. ", caplog)
assert log_has("Exiting the trade forcefully", caplog) assert log_has("Exiting the trade forcefully", caplog)
@ -1305,7 +1305,7 @@ def test_create_stoploss_order_invalid_order(
# Rpc is sending first buy, then sell # Rpc is sending first buy, then sell
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
assert rpc_mock.call_args_list[1][0][0]['sell_reason'] == ExitType.EMERGENCY_SELL.value assert rpc_mock.call_args_list[1][0][0]['sell_reason'] == ExitType.EMERGENCY_EXIT.value
assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market'
@ -1325,7 +1325,7 @@ def test_create_stoploss_order_insufficient_funds(
'last': 1.9 'last': 1.9
}), }),
create_order=MagicMock(side_effect=[ create_order=MagicMock(side_effect=[
limit_order[enter_side(is_short)], limit_order[entry_side(is_short)],
exit_order, exit_order,
]), ]),
get_fee=fee, get_fee=fee,
@ -1364,7 +1364,7 @@ def test_handle_stoploss_on_exchange_trailing(
mocker, default_conf_usdt, fee, is_short, bid, ask, limit_order, stop_price, amt, hang_price mocker, default_conf_usdt, fee, is_short, bid, ask, limit_order, stop_price, amt, hang_price
) -> None: ) -> None:
# When trailing stoploss is set # When trailing stoploss is set
enter_order = limit_order[enter_side(is_short)] enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)] exit_order = limit_order[exit_side(is_short)]
stoploss = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
patch_RPCManager(mocker) patch_RPCManager(mocker)
@ -1485,7 +1485,7 @@ def test_handle_stoploss_on_exchange_trailing(
def test_handle_stoploss_on_exchange_trailing_error( def test_handle_stoploss_on_exchange_trailing_error(
mocker, default_conf_usdt, fee, caplog, limit_order, is_short mocker, default_conf_usdt, fee, caplog, limit_order, is_short
) -> None: ) -> None:
enter_order = limit_order[enter_side(is_short)] enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)] exit_order = limit_order[exit_side(is_short)]
# When trailing stoploss is set # When trailing stoploss is set
stoploss = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
@ -1593,7 +1593,7 @@ def test_stoploss_on_exchange_price_rounding(
def test_handle_stoploss_on_exchange_custom_stop( def test_handle_stoploss_on_exchange_custom_stop(
mocker, default_conf_usdt, fee, is_short, limit_order mocker, default_conf_usdt, fee, is_short, limit_order
) -> None: ) -> None:
enter_order = limit_order[enter_side(is_short)] enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)] exit_order = limit_order[exit_side(is_short)]
# When trailing stoploss is set # When trailing stoploss is set
stoploss = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
@ -1860,10 +1860,10 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.Exchange.fetch_order', mocker.patch('freqtrade.exchange.Exchange.fetch_order',
return_value=limit_order[enter_side(is_short)]) return_value=limit_order[entry_side(is_short)])
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
return_value=limit_order[enter_side(is_short)]['amount']) return_value=limit_order[entry_side(is_short)]['amount'])
trade = MagicMock() trade = MagicMock()
trade.is_short = is_short trade.is_short = is_short
@ -1886,7 +1886,7 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog, is_short) -> None: def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog, is_short) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
order = limit_order[enter_side(is_short)] order = limit_order[entry_side(is_short)]
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
trade = MagicMock() trade = MagicMock()
@ -1909,7 +1909,7 @@ def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, caplog) -> None: def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, caplog) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
order = limit_order[enter_side(is_short)] order = limit_order[entry_side(is_short)]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
@ -1930,7 +1930,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
leverage=1, leverage=1,
) )
trade.orders.append(Order( trade.orders.append(Order(
ft_order_side=enter_side(is_short), ft_order_side=entry_side(is_short),
price=0.01, price=0.01,
order_id=order_id, order_id=order_id,
@ -1980,7 +1980,7 @@ def test_update_trade_state_withorderdict(
default_conf_usdt, trades_for_order, limit_order, fee, mocker, initial_amount, default_conf_usdt, trades_for_order, limit_order, fee, mocker, initial_amount,
has_rounding_fee, is_short, caplog has_rounding_fee, is_short, caplog
): ):
order = limit_order[enter_side(is_short)] order = limit_order[entry_side(is_short)]
trades_for_order[0]['amount'] = initial_amount trades_for_order[0]['amount'] = initial_amount
order_id = "oid_123456" order_id = "oid_123456"
order['id'] = order_id order['id'] = order_id
@ -2006,7 +2006,7 @@ def test_update_trade_state_withorderdict(
) )
trade.orders.append( trade.orders.append(
Order( Order(
ft_order_side=enter_side(is_short), ft_order_side=entry_side(is_short),
ft_pair=trade.pair, ft_pair=trade.pair,
ft_is_open=True, ft_is_open=True,
order_id=order_id, order_id=order_id,
@ -2026,7 +2026,7 @@ def test_update_trade_state_withorderdict(
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit_order, def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit_order,
caplog) -> None: caplog) -> None:
order = limit_order[enter_side(is_short)] order = limit_order[entry_side(is_short)]
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
@ -2107,7 +2107,7 @@ def test_handle_trade(
default_conf_usdt, limit_order_open, limit_order, fee, mocker, is_short, close_profit default_conf_usdt, limit_order_open, limit_order, fee, mocker, is_short, close_profit
) -> None: ) -> None:
open_order = limit_order_open[exit_side(is_short)] open_order = limit_order_open[exit_side(is_short)]
enter_order = limit_order[enter_side(is_short)] enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)] exit_order = limit_order[exit_side(is_short)]
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
@ -2134,7 +2134,7 @@ def test_handle_trade(
assert trade assert trade
time.sleep(0.01) # Race condition fix time.sleep(0.01) # Race condition fix
oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], enter_side(is_short)) oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], entry_side(is_short))
trade.update_trade(oobj) trade.update_trade(oobj)
assert trade.is_open is True assert trade.is_open is True
freqtrade.wallets.update() freqtrade.wallets.update()
@ -2235,7 +2235,7 @@ def test_handle_overlapping_signals(
def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee, mocker, caplog, def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee, mocker, caplog,
is_short) -> None: is_short) -> None:
open_order = limit_order_open[enter_side(is_short)] open_order = limit_order_open[entry_side(is_short)]
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
@ -2268,19 +2268,19 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee,
caplog.clear() caplog.clear()
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
assert freqtrade.handle_trade(trade) assert freqtrade.handle_trade(trade)
assert log_has("ETH/USDT - Required profit reached. sell_type=ExitType.ROI", assert log_has("ETH/USDT - Required profit reached. exit_type=ExitType.ROI",
caplog) caplog)
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_handle_trade_use_sell_signal( def test_handle_trade_use_exit_signal(
default_conf_usdt, ticker_usdt, limit_order_open, fee, mocker, caplog, is_short default_conf_usdt, ticker_usdt, limit_order_open, fee, mocker, caplog, is_short
) -> None: ) -> None:
enter_open_order = limit_order_open[exit_side(is_short)] enter_open_order = limit_order_open[exit_side(is_short)]
exit_open_order = limit_order_open[enter_side(is_short)] exit_open_order = limit_order_open[entry_side(is_short)]
# use_sell_signal is True buy default # use_exit_signal is True buy default
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
patch_RPCManager(mocker) patch_RPCManager(mocker)
mocker.patch.multiple( mocker.patch.multiple(
@ -2310,7 +2310,7 @@ def test_handle_trade_use_sell_signal(
else: else:
patch_get_signal(freqtrade, enter_long=False, exit_long=True) patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) assert freqtrade.handle_trade(trade)
assert log_has("ETH/USDT - Sell signal received. sell_type=ExitType.SELL_SIGNAL", assert log_has("ETH/USDT - Sell signal received. exit_type=ExitType.EXIT_SIGNAL",
caplog) caplog)
@ -2320,7 +2320,7 @@ def test_close_trade(
) -> None: ) -> None:
open_order = limit_order_open[exit_side(is_short)] open_order = limit_order_open[exit_side(is_short)]
enter_order = limit_order[exit_side(is_short)] enter_order = limit_order[exit_side(is_short)]
exit_order = limit_order[enter_side(is_short)] exit_order = limit_order[entry_side(is_short)]
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
@ -2766,7 +2766,7 @@ def test_check_handle_timedout_partial_fee(
assert trades[0].amount == (limit_buy_order_old_partial['amount'] - assert trades[0].amount == (limit_buy_order_old_partial['amount'] -
limit_buy_order_old_partial['remaining']) - 0.023 limit_buy_order_old_partial['remaining']) - 0.023
assert trades[0].open_order_id is None assert trades[0].open_order_id is None
assert trades[0].fee_updated(open_trade.enter_side) assert trades[0].fee_updated(open_trade.entry_side)
assert pytest.approx(trades[0].fee_open) == 0.001 assert pytest.approx(trades[0].fee_open) == 0.001
@ -2853,8 +2853,8 @@ def test_check_handle_timedout_exception(default_conf_usdt, ticker_usdt, open_tr
def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_short) -> None: def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_short) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
l_order = limit_order[enter_side(is_short)] l_order = limit_order[entry_side(is_short)]
cancel_buy_order = deepcopy(limit_order[enter_side(is_short)]) cancel_buy_order = deepcopy(limit_order[entry_side(is_short)])
cancel_buy_order['status'] = 'canceled' cancel_buy_order['status'] = 'canceled'
del cancel_buy_order['filled'] del cancel_buy_order['filled']
@ -2868,7 +2868,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
trade.pair = 'LTC/USDT' trade.pair = 'LTC/USDT'
trade.open_rate = 200 trade.open_rate = 200
trade.is_short = False trade.is_short = False
trade.enter_side = "buy" trade.entry_side = "buy"
l_order['filled'] = 0.0 l_order['filled'] = 0.0
l_order['status'] = 'open' l_order['status'] = 'open'
reason = CANCEL_REASON['TIMEOUT'] reason = CANCEL_REASON['TIMEOUT']
@ -2896,7 +2896,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
assert log_has_re(r"Order .* for .* not cancelled.", caplog) assert log_has_re(r"Order .* for .* not cancelled.", caplog)
# min_pair_stake empty should not crash # min_pair_stake empty should not crash
mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=None) mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=None)
assert not freqtrade.handle_cancel_enter(trade, limit_order[enter_side(is_short)], reason) assert not freqtrade.handle_cancel_enter(trade, limit_order[entry_side(is_short)], reason)
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -2915,11 +2915,11 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho
reason = CANCEL_REASON['TIMEOUT'] reason = CANCEL_REASON['TIMEOUT']
trade = MagicMock() trade = MagicMock()
trade.pair = 'LTC/ETH' trade.pair = 'LTC/ETH'
trade.enter_side = "sell" if is_short else "buy" trade.entry_side = "sell" if is_short else "buy"
assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason) assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason)
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
assert log_has_re( assert log_has_re(
f'{trade.enter_side.capitalize()} order fully cancelled. ' f'{trade.entry_side.capitalize()} order fully cancelled. '
r'Removing .* from database\.', r'Removing .* from database\.',
caplog caplog
) )
@ -2937,7 +2937,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
cancelorder) -> None: cancelorder) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
l_order = limit_order[enter_side(is_short)] l_order = limit_order[entry_side(is_short)]
cancel_order_mock = MagicMock(return_value=cancelorder) cancel_order_mock = MagicMock(return_value=cancelorder)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -2949,9 +2949,9 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
trade = MagicMock() trade = MagicMock()
trade.pair = 'LTC/USDT' trade.pair = 'LTC/USDT'
trade.enter_side = "buy" trade.entry_side = "buy"
trade.open_rate = 200 trade.open_rate = 200
trade.enter_side = "buy" trade.entry_side = "buy"
l_order['filled'] = 0.0 l_order['filled'] = 0.0
l_order['status'] = 'open' l_order['status'] = 'open'
reason = CANCEL_REASON['TIMEOUT'] reason = CANCEL_REASON['TIMEOUT']
@ -3014,7 +3014,7 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
# Message should not be iterated again # Message should not be iterated again
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert trade.exit_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
assert send_msg_mock.call_count == 1 assert send_msg_mock.call_count == 1
@ -3091,7 +3091,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'trade_id': 1, 'trade_id': 1,
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/USDT', 'pair': 'ETH/USDT',
'gain': 'profit', 'gain': 'profit',
@ -3150,7 +3150,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/USDT', 'pair': 'ETH/USDT',
@ -3221,7 +3221,7 @@ def test_execute_trade_exit_custom_exit_price(
freqtrade.execute_trade_exit( freqtrade.execute_trade_exit(
trade=trade, trade=trade,
limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
exit_check=ExitCheckTuple(exit_type=ExitType.SELL_SIGNAL) exit_check=ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)
) )
# Sell price must be different to default bid price # Sell price must be different to default bid price
@ -3232,7 +3232,7 @@ def test_execute_trade_exit_custom_exit_price(
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'trade_id': 1, 'trade_id': 1,
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/USDT', 'pair': 'ETH/USDT',
'direction': 'Short' if trade.is_short else 'Long', 'direction': 'Short' if trade.is_short else 'Long',
@ -3249,8 +3249,8 @@ def test_execute_trade_exit_custom_exit_price(
'profit_ratio': profit_ratio, 'profit_ratio': profit_ratio,
'stake_currency': 'USDT', 'stake_currency': 'USDT',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': ExitType.SELL_SIGNAL.value, 'sell_reason': ExitType.EXIT_SIGNAL.value,
'exit_reason': ExitType.SELL_SIGNAL.value, 'exit_reason': ExitType.EXIT_SIGNAL.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -3299,7 +3299,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/USDT', 'pair': 'ETH/USDT',
@ -3487,15 +3487,9 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
assert trade.is_open is False assert trade.is_open is False
assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value
assert rpc_mock.call_count == 3 assert rpc_mock.call_count == 3
if is_short: assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.ENTRY
assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.SHORT assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.ENTRY_FILL
assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.SHORT_FILL assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.EXIT
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL
else:
assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY
assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -3563,7 +3557,7 @@ def test_execute_trade_exit_market_order(
assert rpc_mock.call_count == 3 assert rpc_mock.call_count == 3
last_msg = rpc_mock.call_args_list[-2][0][0] last_msg = rpc_mock.call_args_list[-2][0][0]
assert { assert {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/USDT', 'pair': 'ETH/USDT',
@ -3628,27 +3622,27 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
assert mock_insuf.call_count == 1 assert mock_insuf.call_count == 1
@pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type,is_short', [ @pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,exit_type,is_short', [
# Enable profit # Enable profit
(True, 2.18, 2.2, False, True, ExitType.SELL_SIGNAL.value, False), (True, 2.18, 2.2, False, True, ExitType.EXIT_SIGNAL.value, False),
(True, 2.18, 2.2, False, True, ExitType.SELL_SIGNAL.value, True), (True, 2.18, 2.2, False, True, ExitType.EXIT_SIGNAL.value, True),
# # Disable profit # # Disable profit
(False, 3.19, 3.2, True, False, ExitType.SELL_SIGNAL.value, False), (False, 3.19, 3.2, True, False, ExitType.EXIT_SIGNAL.value, False),
(False, 3.19, 3.2, True, False, ExitType.SELL_SIGNAL.value, True), (False, 3.19, 3.2, True, False, ExitType.EXIT_SIGNAL.value, True),
# # Enable loss # # Enable loss
# # * Shouldn't this be ExitType.STOP_LOSS.value # # * Shouldn't this be ExitType.STOP_LOSS.value
(True, 0.21, 0.22, False, False, None, False), (True, 0.21, 0.22, False, False, None, False),
(True, 2.41, 2.42, False, False, None, True), (True, 2.41, 2.42, False, False, None, True),
# Disable loss # Disable loss
(False, 0.10, 0.22, True, False, ExitType.SELL_SIGNAL.value, False), (False, 0.10, 0.22, True, False, ExitType.EXIT_SIGNAL.value, False),
(False, 0.10, 0.22, True, False, ExitType.SELL_SIGNAL.value, True), (False, 0.10, 0.22, True, False, ExitType.EXIT_SIGNAL.value, True),
]) ])
def test_sell_profit_only( def test_exit_profit_only(
default_conf_usdt, limit_order, limit_order_open, is_short, default_conf_usdt, limit_order, limit_order_open, is_short,
fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None: fee, mocker, profit_only, bid, ask, handle_first, handle_second, exit_type) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
eside = enter_side(is_short) eside = entry_side(is_short)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={ fetch_ticker=MagicMock(return_value={
@ -3663,13 +3657,14 @@ def test_sell_profit_only(
get_fee=fee, get_fee=fee,
) )
default_conf_usdt.update({ default_conf_usdt.update({
'use_sell_signal': True, 'use_exit_signal': True,
'sell_profit_only': profit_only, 'exit_profit_only': profit_only,
'sell_profit_offset': 0.1, 'exit_profit_offset': 0.1,
}) })
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
if sell_type == ExitType.SELL_SIGNAL.value: freqtrade.strategy.custom_exit = MagicMock(return_value=None)
if exit_type == ExitType.EXIT_SIGNAL.value:
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
else: else:
freqtrade.strategy.stop_loss_reached = MagicMock(return_value=ExitCheckTuple( freqtrade.strategy.stop_loss_reached = MagicMock(return_value=ExitCheckTuple(
@ -3677,15 +3672,20 @@ def test_sell_profit_only(
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
trade.is_short = is_short assert trade.is_short == is_short
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside) oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
trade.update_trade(oobj) trade.update_trade(oobj)
freqtrade.wallets.update() freqtrade.wallets.update()
if profit_only:
assert freqtrade.handle_trade(trade) is False
# Custom-exit is called
freqtrade.strategy.custom_exit.call_count == 1
patch_get_signal(freqtrade, enter_long=False, exit_short=is_short, exit_long=not is_short) patch_get_signal(freqtrade, enter_long=False, exit_short=is_short, exit_long=not is_short)
assert freqtrade.handle_trade(trade) is handle_first assert freqtrade.handle_trade(trade) is handle_first
if handle_second: if handle_second:
freqtrade.strategy.sell_profit_offset = 0.0 freqtrade.strategy.exit_profit_offset = 0.0
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
@ -3805,11 +3805,11 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee,
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_open, is_short, def test_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limit_order_open, is_short,
fee, mocker) -> None: fee, mocker) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
eside = enter_side(is_short) eside = entry_side(is_short)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={ fetch_ticker=MagicMock(return_value={
@ -3823,7 +3823,7 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_op
]), ]),
get_fee=fee, get_fee=fee,
) )
default_conf_usdt['ignore_roi_if_buy_signal'] = True default_conf_usdt['ignore_roi_if_entry_signal'] = True
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
@ -3869,7 +3869,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
'last': 2.0 'last': 2.0
}), }),
create_order=MagicMock(side_effect=[ create_order=MagicMock(side_effect=[
limit_order_open[enter_side(is_short)], limit_order_open[entry_side(is_short)],
{'id': 1234553382}, {'id': 1234553382},
]), ]),
get_fee=fee, get_fee=fee,
@ -3927,10 +3927,10 @@ def test_trailing_stop_loss_positive(
default_conf_usdt, limit_order, limit_order_open, default_conf_usdt, limit_order, limit_order_open,
offset, fee, caplog, mocker, trail_if_reached, second_sl, is_short offset, fee, caplog, mocker, trail_if_reached, second_sl, is_short
) -> None: ) -> None:
enter_price = limit_order[enter_side(is_short)]['price'] enter_price = limit_order[entry_side(is_short)]['price']
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
eside = enter_side(is_short) eside = entry_side(is_short)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={ fetch_ticker=MagicMock(return_value={
@ -4022,11 +4022,11 @@ def test_trailing_stop_loss_positive(
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_open, def test_disable_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limit_order_open,
is_short, fee, mocker) -> None: is_short, fee, mocker) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
eside = enter_side(is_short) eside = entry_side(is_short)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={ fetch_ticker=MagicMock(return_value={
@ -4043,7 +4043,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_
_is_dry_limit_order_filled=MagicMock(return_value=False), _is_dry_limit_order_filled=MagicMock(return_value=False),
) )
default_conf_usdt['exit_pricing'] = { default_conf_usdt['exit_pricing'] = {
'ignore_roi_if_buy_signal': False 'ignore_roi_if_entry_signal': False
} }
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
@ -4430,7 +4430,7 @@ def test_order_book_depth_of_market(
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker_usdt, fetch_ticker=ticker_usdt,
create_order=MagicMock(return_value=limit_order_open[enter_side(is_short)]), create_order=MagicMock(return_value=limit_order_open[entry_side(is_short)]),
get_fee=fee, get_fee=fee,
) )
@ -4455,7 +4455,7 @@ def test_order_book_depth_of_market(
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object( oobj = Order.parse_from_ccxt_object(
limit_order_open[enter_side(is_short)], 'ADA/USDT', enter_side(is_short)) limit_order_open[entry_side(is_short)], 'ADA/USDT', entry_side(is_short))
trade.update_trade(oobj) trade.update_trade(oobj)
assert trade.open_rate == ticker_usdt.return_value[ticker_side] assert trade.open_rate == ticker_usdt.return_value[ticker_side]
@ -4644,7 +4644,7 @@ def test_cancel_all_open_orders(mocker, default_conf_usdt, fee, limit_order, lim
side_effect=[ side_effect=[
ExchangeError(), ExchangeError(),
limit_order[exit_side(is_short)], limit_order[exit_side(is_short)],
limit_order_open[enter_side(is_short)], limit_order_open[entry_side(is_short)],
limit_order_open[exit_side(is_short)], limit_order_open[exit_side(is_short)],
] ]
) )
@ -4757,7 +4757,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf_usdt, f
for trade in trades: for trade in trades:
if trade.is_open: if trade.is_open:
# Exclude Trade 4 - as the order is still open. # Exclude Trade 4 - as the order is still open.
if trade.select_order(enter_side(is_short), False): if trade.select_order(entry_side(is_short), False):
assert trade.fee_open_cost is not None assert trade.fee_open_cost is not None
assert trade.fee_open_currency is not None assert trade.fee_open_currency is not None
else: else:
@ -5014,7 +5014,7 @@ def test_update_funding_fees(
# SETUP # SETUP
time_machine.move_to("2021-09-01 00:00:00 +00:00") time_machine.move_to("2021-09-01 00:00:00 +00:00")
open_order = limit_order_open[enter_side(is_short)] open_order = limit_order_open[entry_side(is_short)]
open_exit_order = limit_order_open[exit_side(is_short)] open_exit_order = limit_order_open[exit_side(is_short)]
bid = 0.11 bid = 0.11
enter_rate_mock = MagicMock(return_value=bid) enter_rate_mock = MagicMock(return_value=bid)

View File

@ -53,7 +53,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
# Sell 3rd trade (not called for the first trade) # Sell 3rd trade (not called for the first trade)
should_sell_mock = MagicMock(side_effect=[ should_sell_mock = MagicMock(side_effect=[
ExitCheckTuple(exit_type=ExitType.NONE), ExitCheckTuple(exit_type=ExitType.NONE),
ExitCheckTuple(exit_type=ExitType.SELL_SIGNAL)] ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]
) )
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
@ -123,7 +123,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
assert trade.is_open assert trade.is_open
trade = trades[2] trade = trades[2]
assert trade.exit_reason == ExitType.SELL_SIGNAL.value assert trade.exit_reason == ExitType.EXIT_SIGNAL.value
assert not trade.is_open assert not trade.is_open
@ -139,7 +139,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
one trade was sold at a loss. 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['force_entry_enable'] = True
default_conf['stake_amount'] = 'unlimited' default_conf['stake_amount'] = 'unlimited'
default_conf['tradable_balance_ratio'] = balance_ratio default_conf['tradable_balance_ratio'] = balance_ratio
default_conf['dry_run_wallet'] = 1000 default_conf['dry_run_wallet'] = 1000
@ -161,7 +161,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
) )
should_sell_mock = MagicMock(side_effect=[ should_sell_mock = MagicMock(side_effect=[
ExitCheckTuple(exit_type=ExitType.NONE), ExitCheckTuple(exit_type=ExitType.NONE),
ExitCheckTuple(exit_type=ExitType.SELL_SIGNAL), ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL),
ExitCheckTuple(exit_type=ExitType.NONE), ExitCheckTuple(exit_type=ExitType.NONE),
ExitCheckTuple(exit_type=ExitType.NONE), ExitCheckTuple(exit_type=ExitType.NONE),
ExitCheckTuple(exit_type=ExitType.NONE)] ExitCheckTuple(exit_type=ExitType.NONE)]

View File

@ -76,7 +76,7 @@ def test_init_dryrun_db(default_conf, tmpdir):
@pytest.mark.parametrize('is_short', [False, True]) @pytest.mark.parametrize('is_short', [False, True])
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_enter_exit_side(fee, is_short): def test_enter_exit_side(fee, is_short):
enter_side, exit_side = ("sell", "buy") if is_short else ("buy", "sell") entry_side, exit_side = ("sell", "buy") if is_short else ("buy", "sell")
trade = Trade( trade = Trade(
id=2, id=2,
pair='ADA/USDT', pair='ADA/USDT',
@ -92,7 +92,7 @@ def test_enter_exit_side(fee, is_short):
leverage=2.0, leverage=2.0,
trading_mode=margin trading_mode=margin
) )
assert trade.enter_side == enter_side assert trade.entry_side == entry_side
assert trade.exit_side == exit_side assert trade.exit_side == exit_side
assert trade.trade_direction == 'short' if is_short else 'long' assert trade.trade_direction == 'short' if is_short else 'long'
@ -456,7 +456,7 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_
enter_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt enter_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
exit_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt exit_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt
enter_side, exit_side = ("sell", "buy") if is_short else ("buy", "sell") entry_side, exit_side = ("sell", "buy") if is_short else ("buy", "sell")
trade = Trade( trade = Trade(
id=2, id=2,
@ -479,13 +479,13 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_
assert trade.close_date is None assert trade.close_date is None
trade.open_order_id = 'something' trade.open_order_id = 'something'
oobj = Order.parse_from_ccxt_object(enter_order, 'ADA/USDT', enter_side) oobj = Order.parse_from_ccxt_object(enter_order, 'ADA/USDT', entry_side)
trade.update_trade(oobj) trade.update_trade(oobj)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.open_rate == open_rate assert trade.open_rate == open_rate
assert trade.close_profit is None assert trade.close_profit is None
assert trade.close_date is None assert trade.close_date is None
assert log_has_re(f"LIMIT_{enter_side.upper()} has been fulfilled for " assert log_has_re(f"LIMIT_{entry_side.upper()} has been fulfilled for "
r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, " r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, "
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, " f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, "
r"open_since=.*\).", r"open_since=.*\).",
@ -1209,6 +1209,27 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
PRIMARY KEY (id), PRIMARY KEY (id),
CHECK (is_open IN (0, 1)) CHECK (is_open IN (0, 1))
);""" );"""
create_table_order = """CREATE TABLE orders (
id INTEGER NOT NULL,
ft_trade_id INTEGER,
ft_order_side VARCHAR(25) NOT NULL,
ft_pair VARCHAR(25) NOT NULL,
ft_is_open BOOLEAN NOT NULL,
order_id VARCHAR(255) NOT NULL,
status VARCHAR(255),
symbol VARCHAR(25),
order_type VARCHAR(50),
side VARCHAR(25),
price FLOAT,
amount FLOAT,
filled FLOAT,
remaining FLOAT,
cost FLOAT,
order_date DATETIME,
order_filled_date DATETIME,
order_update_date DATETIME,
PRIMARY KEY (id)
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee, insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, stake_amount, amount, open_date, open_rate, stake_amount, amount, open_date,
stop_loss, initial_stop_loss, max_rate, ticker_interval, stop_loss, initial_stop_loss, max_rate, ticker_interval,
@ -1222,15 +1243,66 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
stake=default_conf.get("stake_amount"), stake=default_conf.get("stake_amount"),
amount=amount amount=amount
) )
insert_orders = f"""
insert into orders (
ft_trade_id,
ft_order_side,
ft_pair,
ft_is_open,
order_id,
status,
symbol,
order_type,
side,
price,
amount,
filled,
remaining,
cost)
values (
1,
'buy',
'ETC/BTC',
0,
'buy_order',
'closed',
'ETC/BTC',
'limit',
'buy',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
),
(
1,
'stoploss',
'ETC/BTC',
0,
'stop_order_id222',
'closed',
'ETC/BTC',
'limit',
'sell',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
)
"""
engine = create_engine('sqlite://') engine = create_engine('sqlite://')
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine) mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
# Create table using the old format # Create table using the old format
with engine.begin() as connection: with engine.begin() as connection:
connection.execute(text(create_table_old)) connection.execute(text(create_table_old))
connection.execute(text(create_table_order))
connection.execute(text("create index ix_trades_is_open on trades(is_open)")) connection.execute(text("create index ix_trades_is_open on trades(is_open)"))
connection.execute(text("create index ix_trades_pair on trades(pair)")) connection.execute(text("create index ix_trades_pair on trades(pair)"))
connection.execute(text(insert_table_old)) connection.execute(text(insert_table_old))
connection.execute(text(insert_orders))
# fake previous backup # fake previous backup
connection.execute(text("create table trades_bak as select * from trades")) connection.execute(text("create table trades_bak as select * from trades"))
@ -1267,8 +1339,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.open_trade_value == trade._calc_open_trade_value() assert trade.open_trade_value == trade._calc_open_trade_value()
assert trade.close_profit_abs is None assert trade.close_profit_abs is None
assert log_has("Moving open orders to Orders table.", caplog) orders = trade.orders
orders = Order.query.all()
assert len(orders) == 2 assert len(orders) == 2
assert orders[0].order_id == 'buy_order' assert orders[0].order_id == 'buy_order'
assert orders[0].ft_order_side == 'buy' assert orders[0].ft_order_side == 'buy'
@ -1277,7 +1348,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert orders[1].ft_order_side == 'stoploss' assert orders[1].ft_order_side == 'stoploss'
def test_migrate_mid_state(mocker, default_conf, fee, caplog): def test_migrate_too_old(mocker, default_conf, fee, caplog):
""" """
Test Database migration (starting with new pairformat) Test Database migration (starting with new pairformat)
""" """
@ -1301,6 +1372,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
PRIMARY KEY (id), PRIMARY KEY (id),
CHECK (is_open IN (0, 1)) CHECK (is_open IN (0, 1))
);""" );"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close,
open_rate, stake_amount, amount, open_date) open_rate, stake_amount, amount, open_date)
VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee}, VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee},
@ -1319,27 +1391,9 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
connection.execute(text(insert_table_old)) connection.execute(text(insert_table_old))
# Run init to test migration # Run init to test migration
with pytest.raises(OperationalException, match=r'Your database seems to be very old'):
init_db(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first()
assert trade.fee_open == fee.return_value
assert trade.fee_close == fee.return_value
assert trade.open_rate_requested is None
assert trade.close_rate_requested is None
assert trade.is_open == 1
assert trade.amount == amount
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "binance"
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.open_trade_value == trade._calc_open_trade_value()
assert log_has("trying trades_bak0", caplog)
assert log_has("Running database migration for trades - backup: trades_bak0, orders_bak0",
caplog)
def test_migrate_get_last_sequence_ids(): def test_migrate_get_last_sequence_ids():
engine = MagicMock() engine = MagicMock()
@ -1561,6 +1615,8 @@ def test_to_json(fee):
assert result == {'trade_id': None, assert result == {'trade_id': None,
'pair': 'ADA/USDT', 'pair': 'ADA/USDT',
'base_currency': 'ADA',
'quote_currency': 'USDT',
'is_open': None, 'is_open': None,
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
'open_timestamp': int(trade.open_date.timestamp() * 1000), 'open_timestamp': int(trade.open_date.timestamp() * 1000),
@ -1591,7 +1647,7 @@ def test_to_json(fee):
'profit_abs': None, 'profit_abs': None,
'sell_reason': None, 'sell_reason': None,
'exit_reason': None, 'exit_reason': None,
'sell_order_status': None, 'exit_order_status': None,
'stop_loss_abs': None, 'stop_loss_abs': None,
'stop_loss_ratio': None, 'stop_loss_ratio': None,
'stop_loss_pct': None, 'stop_loss_pct': None,
@ -1637,6 +1693,8 @@ def test_to_json(fee):
assert result == {'trade_id': None, assert result == {'trade_id': None,
'pair': 'XRP/BTC', 'pair': 'XRP/BTC',
'base_currency': 'XRP',
'quote_currency': 'BTC',
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
'open_timestamp': int(trade.open_date.timestamp() * 1000), 'open_timestamp': int(trade.open_date.timestamp() * 1000),
'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"), 'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"),
@ -1678,7 +1736,7 @@ def test_to_json(fee):
'open_trade_value': 12.33075, 'open_trade_value': 12.33075,
'sell_reason': None, 'sell_reason': None,
'exit_reason': None, 'exit_reason': None,
'sell_order_status': None, 'exit_order_status': None,
'strategy': None, 'strategy': None,
'buy_tag': 'buys_signal_001', 'buy_tag': 'buys_signal_001',
'enter_tag': 'buys_signal_001', 'enter_tag': 'buys_signal_001',
@ -2135,19 +2193,19 @@ def test_select_order(fee, is_short):
trades = Trade.get_trades().all() trades = Trade.get_trades().all()
# Open buy order, no sell order # Open buy order, no sell order
order = trades[0].select_order(trades[0].enter_side, True) order = trades[0].select_order(trades[0].entry_side, True)
assert order is None assert order is None
order = trades[0].select_order(trades[0].enter_side, False) order = trades[0].select_order(trades[0].entry_side, False)
assert order is not None assert order is not None
order = trades[0].select_order(trades[0].exit_side, None) order = trades[0].select_order(trades[0].exit_side, None)
assert order is None assert order is None
# closed buy order, and open sell order # closed buy order, and open sell order
order = trades[1].select_order(trades[1].enter_side, True) order = trades[1].select_order(trades[1].entry_side, True)
assert order is None assert order is None
order = trades[1].select_order(trades[1].enter_side, False) order = trades[1].select_order(trades[1].entry_side, False)
assert order is not None assert order is not None
order = trades[1].select_order(trades[1].enter_side, None) order = trades[1].select_order(trades[1].entry_side, None)
assert order is not None assert order is not None
order = trades[1].select_order(trades[1].exit_side, True) order = trades[1].select_order(trades[1].exit_side, True)
assert order is None assert order is None
@ -2155,15 +2213,15 @@ def test_select_order(fee, is_short):
assert order is not None assert order is not None
# Has open buy order # Has open buy order
order = trades[3].select_order(trades[3].enter_side, True) order = trades[3].select_order(trades[3].entry_side, True)
assert order is not None assert order is not None
order = trades[3].select_order(trades[3].enter_side, False) order = trades[3].select_order(trades[3].entry_side, False)
assert order is None assert order is None
# Open sell order # Open sell order
order = trades[4].select_order(trades[4].enter_side, True) order = trades[4].select_order(trades[4].entry_side, True)
assert order is None assert order is None
order = trades[4].select_order(trades[4].enter_side, False) order = trades[4].select_order(trades[4].entry_side, False)
assert order is not None assert order is not None
trades[4].orders[1].ft_order_side = trades[4].exit_side trades[4].orders[1].ft_order_side = trades[4].exit_side
@ -2386,7 +2444,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short):
o1_cost = o1_amount * o1_rate o1_cost = o1_amount * o1_rate
o1_fee_cost = o1_cost * fee.return_value o1_fee_cost = o1_cost * fee.return_value
o1_trade_val = o1_cost - o1_fee_cost if is_short else o1_cost + o1_fee_cost o1_trade_val = o1_cost - o1_fee_cost if is_short else o1_cost + o1_fee_cost
enter_side = "sell" if is_short else "buy" entry_side = "sell" if is_short else "buy"
exit_side = "buy" if is_short else "sell" exit_side = "buy" if is_short else "sell"
trade = Trade( trade = Trade(
@ -2402,16 +2460,16 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short):
is_short=is_short, is_short=is_short,
leverage=1.0, leverage=1.0,
) )
trade.update_fee(o1_fee_cost, 'BNB', fee.return_value, enter_side) trade.update_fee(o1_fee_cost, 'BNB', fee.return_value, entry_side)
# Check with 1 order # Check with 1 order
order1 = Order( order1 = Order(
ft_order_side=enter_side, ft_order_side=entry_side,
ft_pair=trade.pair, ft_pair=trade.pair,
ft_is_open=False, ft_is_open=False,
status="closed", status="closed",
symbol=trade.pair, symbol=trade.pair,
order_type="market", order_type="market",
side=enter_side, side=entry_side,
price=o1_rate, price=o1_rate,
average=o1_rate, average=o1_rate,
filled=o1_amount, filled=o1_amount,
@ -2432,13 +2490,13 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short):
assert trade.nr_of_successful_entries == 1 assert trade.nr_of_successful_entries == 1
order2 = Order( order2 = Order(
ft_order_side=enter_side, ft_order_side=entry_side,
ft_pair=trade.pair, ft_pair=trade.pair,
ft_is_open=True, ft_is_open=True,
status="open", status="open",
symbol=trade.pair, symbol=trade.pair,
order_type="market", order_type="market",
side=enter_side, side=entry_side,
price=o1_rate, price=o1_rate,
average=o1_rate, average=o1_rate,
filled=o1_amount, filled=o1_amount,
@ -2460,13 +2518,13 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short):
# Let's try with some other orders # Let's try with some other orders
order3 = Order( order3 = Order(
ft_order_side=enter_side, ft_order_side=entry_side,
ft_pair=trade.pair, ft_pair=trade.pair,
ft_is_open=False, ft_is_open=False,
status="cancelled", status="cancelled",
symbol=trade.pair, symbol=trade.pair,
order_type="market", order_type="market",
side=enter_side, side=entry_side,
price=1, price=1,
average=2, average=2,
filled=0, filled=0,
@ -2487,13 +2545,13 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short):
assert trade.nr_of_successful_entries == 1 assert trade.nr_of_successful_entries == 1
order4 = Order( order4 = Order(
ft_order_side=enter_side, ft_order_side=entry_side,
ft_pair=trade.pair, ft_pair=trade.pair,
ft_is_open=False, ft_is_open=False,
status="closed", status="closed",
symbol=trade.pair, symbol=trade.pair,
order_type="market", order_type="market",
side=enter_side, side=entry_side,
price=o1_rate, price=o1_rate,
average=o1_rate, average=o1_rate,
filled=o1_amount, filled=o1_amount,
@ -2542,13 +2600,13 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short):
# Check with 1 order # Check with 1 order
order_noavg = Order( order_noavg = Order(
ft_order_side=enter_side, ft_order_side=entry_side,
ft_pair=trade.pair, ft_pair=trade.pair,
ft_is_open=False, ft_is_open=False,
status="closed", status="closed",
symbol=trade.pair, symbol=trade.pair,
order_type="market", order_type="market",
side=enter_side, side=entry_side,
price=o1_rate, price=o1_rate,
average=None, average=None,
filled=o1_amount, filled=o1_amount,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
{
// This file fails as it's loading itself over and over
"add_config_files": [
"./recursive.json"
]
}

View File

@ -0,0 +1,12 @@
{
"stake_currency": "",
"dry_run": true,
"exchange": {
"name": "",
"key": "",
"secret": "",
"pair_whitelist": [],
"ccxt_async_config": {
}
}
}

View File

@ -0,0 +1,18 @@
{
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing":{
"price_side": "other",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0
}
}

View File

@ -0,0 +1,21 @@
{
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing":{
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0
},
"add_config_files": [
"./test_pricing2_conf.json"
]
}

View File

@ -0,0 +1,6 @@
{
"add_config_files": [
"test_base_config.json",
"test_pricing_conf.json"
]
}