Merge branch 'develop' into partial_sell2
This commit is contained in:
commit
07af20a55f
21
.pre-commit-config.yaml
Normal file
21
.pre-commit-config.yaml
Normal 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]
|
@ -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.
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
@ -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
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
@ -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 `exit_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` exit 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
|
||||||
|
|
||||||
|
@ -92,7 +92,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 +105,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 +115,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
|
||||||
@ -166,7 +166,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
|
||||||
@ -198,10 +198,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`
|
||||||
@ -330,10 +330,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
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -342,14 +342,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.
|
||||||
|
|
||||||
@ -376,18 +376,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:
|
||||||
|
|
||||||
@ -395,9 +394,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,
|
||||||
@ -411,9 +410,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
|
||||||
@ -436,7 +435,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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -84,16 +84,16 @@ 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 exit 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` or `exit_profit_only=True` while profit is below `exit_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
|
||||||
|
|
||||||
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):
|
||||||
@ -665,7 +665,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
|
||||||
|
@ -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:
|
||||||
|
|
||||||
|
@ -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 : `force_exit`, `force_enter`, `emergency_exit` 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)
|
||||||
@ -27,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`
|
||||||
@ -61,8 +63,12 @@ You can use the quick summary as checklist. Please refer to the detailed section
|
|||||||
* `sell` -> `exit`
|
* `sell` -> `exit`
|
||||||
* `sell_fill` -> `exit_fill`
|
* `sell_fill` -> `exit_fill`
|
||||||
* `sell_cancel` -> `exit_cancel`
|
* `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
|
||||||
|
|
||||||
@ -331,6 +337,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 = {
|
||||||
@ -351,15 +358,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`.
|
||||||
|
@ -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)
|
||||||
@ -285,13 +286,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`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -94,8 +94,8 @@ def _validate_unlimited_amount(conf: Dict[str, Any]) -> None:
|
|||||||
:raise: OperationalException if config validation failed
|
:raise: OperationalException if config validation failed
|
||||||
"""
|
"""
|
||||||
if (not conf.get('edge', {}).get('enabled')
|
if (not conf.get('edge', {}).get('enabled')
|
||||||
and conf.get('max_open_trades') == float('inf')
|
and conf.get('max_open_trades') == float('inf')
|
||||||
and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT):
|
and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT):
|
||||||
raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.")
|
raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.")
|
||||||
|
|
||||||
|
|
||||||
@ -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')
|
||||||
|
@ -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: {}')
|
||||||
|
|
||||||
|
@ -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,16 +74,12 @@ 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')
|
||||||
|
|
||||||
|
process_deprecated_setting(config, None, 'forcebuy_enable', None, 'force_entry_enable')
|
||||||
|
|
||||||
# New settings
|
# New settings
|
||||||
if config.get('telegram'):
|
if config.get('telegram'):
|
||||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell',
|
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell',
|
||||||
@ -109,13 +107,18 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
|||||||
'webhook', 'webhookexitfill')
|
'webhook', 'webhookexitfill')
|
||||||
|
|
||||||
# Legacy way - having them in experimental ...
|
# Legacy way - having them in experimental ...
|
||||||
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')
|
|
||||||
|
|
||||||
|
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(
|
||||||
|
@ -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,21 @@ def load_config_file(path: str) -> Dict[str, Any]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_files(files: List[str]) -> Dict[str, Any]:
|
||||||
|
|
||||||
|
config: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
return deepcopy(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)
|
||||||
|
|
||||||
|
config['config_files'] = files
|
||||||
|
|
||||||
|
return config
|
||||||
|
@ -86,8 +86,8 @@ 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 = {
|
||||||
@ -149,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},
|
||||||
@ -216,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'},
|
||||||
@ -358,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',
|
||||||
|
@ -192,7 +192,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:
|
||||||
@ -331,12 +331,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)
|
||||||
@ -365,7 +365,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:
|
||||||
@ -570,9 +570,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}"
|
||||||
@ -619,6 +619,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
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:
|
||||||
@ -949,8 +950,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)
|
||||||
|
|
||||||
@ -1159,7 +1160,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)
|
||||||
|
|
||||||
@ -1200,7 +1201,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:
|
||||||
@ -1239,7 +1240,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
|
||||||
@ -1270,7 +1271,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()
|
||||||
@ -1405,7 +1406,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
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_EXIT:
|
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, sub_trade_amt or trade.amount)
|
amount = self._safe_exit_amount(trade.pair, sub_trade_amt or trade.amount)
|
||||||
time_in_force = self.strategy.order_time_in_force['exit']
|
time_in_force = self.strategy.order_time_in_force['exit']
|
||||||
@ -1618,7 +1619,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
if order.get('status') in constants.NON_OPEN_EXCHANGE_STATES:
|
if order.get('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()
|
||||||
|
@ -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
|
||||||
@ -421,38 +420,38 @@ class Backtesting:
|
|||||||
# When forceselling with ROI=-1, the roi time will always be equal to trade_dur.
|
# When forceselling 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:
|
||||||
@ -511,7 +510,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:
|
||||||
@ -520,15 +519,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:
|
||||||
@ -536,7 +535,7 @@ 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:
|
||||||
close_rate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
close_rate = 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 close_rate
|
# call the custom exit price,with default value as previous close_rate
|
||||||
@ -553,9 +552,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:
|
||||||
close_rate = min(close_rate, sell_row[HIGH_IDX])
|
close_rate = min(close_rate, row[HIGH_IDX])
|
||||||
else:
|
else:
|
||||||
close_rate = max(close_rate, sell_row[LOW_IDX])
|
close_rate = max(close_rate, 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']
|
||||||
|
|
||||||
@ -571,13 +570,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]
|
||||||
|
|
||||||
return self._exit_trade(trade, sell_row, close_rate)
|
return self._exit_trade(trade, sell_row, close_rate)
|
||||||
|
|
||||||
@ -612,8 +611,8 @@ class Backtesting:
|
|||||||
trade.orders.append(order)
|
trade.orders.append(order)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
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(
|
||||||
@ -634,13 +633,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():
|
||||||
@ -651,7 +650,7 @@ 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],
|
||||||
@ -794,8 +793,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,
|
||||||
@ -877,7 +876,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.
|
||||||
@ -992,7 +991,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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -374,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:
|
||||||
@ -414,7 +420,7 @@ 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,
|
||||||
@ -604,7 +610,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
|
||||||
@ -697,7 +703,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:
|
||||||
@ -714,7 +720,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
|
||||||
@ -960,7 +966,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:
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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 forcesell(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'])
|
||||||
|
@ -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]]
|
||||||
|
@ -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),
|
||||||
@ -684,7 +684,7 @@ 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 forcesell <id>.
|
||||||
Sells the given trade at current price
|
Sells the given trade at current price
|
||||||
@ -695,7 +695,7 @@ class RPC:
|
|||||||
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_EXIT'])
|
trade, order, CANCEL_REASON['FORCE_EXIT'])
|
||||||
|
|
||||||
@ -709,7 +709,7 @@ class RPC:
|
|||||||
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_EXIT)
|
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)
|
||||||
@ -732,7 +732,7 @@ 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_forcesell(trade)
|
||||||
@ -744,14 +744,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 +781,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 +791,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]]:
|
||||||
"""
|
"""
|
||||||
|
@ -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,7 @@ 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_enter_inline),
|
||||||
]
|
]
|
||||||
for handle in handles:
|
for handle in handles:
|
||||||
self._updater.dispatcher.add_handler(handle)
|
self._updater.dispatcher.add_handler(handle)
|
||||||
@ -233,11 +233,11 @@ class Telegram(RPCHandler):
|
|||||||
is_fill = msg['type'] in [RPCMessageType.ENTRY_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['direction'] == 'Long'
|
entry_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
|
||||||
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 ""
|
||||||
@ -974,7 +974,7 @@ 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 /forcesell <id>.
|
||||||
Sells the given trade at current price
|
Sells the given trade at current price
|
||||||
@ -988,20 +988,20 @@ class Telegram(RPCHandler):
|
|||||||
self._send_msg("You must specify a trade-id or 'all'.")
|
self._send_msg("You must specify a trade-id or 'all'.")
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
msg = self._rpc._rpc_forceexit(trade_id)
|
msg = self._rpc._rpc_force_exit(trade_id)
|
||||||
self._send_msg('Forceexit Result: `{result}`'.format(**msg))
|
self._send_msg('Force_exit Result: `{result}`'.format(**msg))
|
||||||
|
|
||||||
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_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:
|
||||||
@ -1009,7 +1009,7 @@ 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(buttons: List[InlineKeyboardButton],
|
||||||
@ -1017,7 +1017,7 @@ class Telegram(RPCHandler):
|
|||||||
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>
|
||||||
@ -1029,7 +1029,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 = [
|
||||||
@ -1407,23 +1407,25 @@ 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 = (
|
||||||
"_BotControl_\n"
|
"_BotControl_\n"
|
||||||
"------------\n"
|
"------------\n"
|
||||||
"*/start:* `Starts the trader`\n"
|
"*/start:* `Starts the trader`\n"
|
||||||
"*/stop:* Stops the trader\n"
|
"*/stop:* Stops the trader\n"
|
||||||
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
|
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
|
||||||
|
# TODO: forceenter forceshort forcelong missing
|
||||||
"*/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 "
|
||||||
|
@ -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
|
||||||
@ -871,7 +871,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
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))
|
||||||
|
|
||||||
@ -881,10 +881,10 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
current_rate = rate
|
current_rate = rate
|
||||||
current_profit = trade.calc_profit_ratio(current_rate)
|
current_profit = trade.calc_profit_ratio(current_rate)
|
||||||
|
|
||||||
if (self.sell_profit_only and current_profit <= self.sell_profit_offset):
|
if (self.exit_profit_only and current_profit <= self.exit_profit_offset):
|
||||||
# sell_profit_only and profit doesn't reach the offset - ignore sell signal
|
# exit_profit_only and profit doesn't reach the offset - ignore sell signal
|
||||||
pass
|
pass
|
||||||
elif self.use_sell_signal and not enter:
|
elif self.use_exit_signal and not enter:
|
||||||
if exit_:
|
if exit_:
|
||||||
exit_signal = ExitType.EXIT_SIGNAL
|
exit_signal = ExitType.EXIT_SIGNAL
|
||||||
else:
|
else:
|
||||||
@ -1049,7 +1049,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:
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
@ -7,6 +7,7 @@ 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
|
||||||
@ -26,4 +27,4 @@ types-requests==2.27.16
|
|||||||
types-tabulate==0.8.6
|
types-tabulate==0.8.6
|
||||||
|
|
||||||
# Extensions to datetime library
|
# Extensions to datetime library
|
||||||
types-python-dateutil==2.8.10
|
types-python-dateutil==2.8.10
|
||||||
|
@ -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,7 +273,7 @@ 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 forcesell(self, tradeid):
|
||||||
"""Force-sell a trade.
|
"""Force-sell a trade.
|
||||||
|
7
setup.sh
7
setup.sh
@ -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,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install tab lib
|
# Install tab lib
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -821,7 +821,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
|
|||||||
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_exit_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)
|
||||||
|
@ -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'))
|
||||||
@ -741,7 +741,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'))
|
||||||
@ -808,7 +808,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'))
|
||||||
@ -834,7 +834,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'))
|
||||||
@ -879,7 +879,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)
|
||||||
@ -1152,10 +1152,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={
|
||||||
@ -1229,10 +1229,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'],
|
||||||
@ -1347,10 +1347,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)
|
||||||
@ -1451,10 +1451,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'],
|
||||||
@ -1558,10 +1558,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={
|
||||||
|
@ -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'))
|
||||||
|
@ -779,7 +779,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()
|
||||||
@ -806,29 +806,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
|
||||||
@ -857,7 +857,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
|
||||||
|
|
||||||
@ -885,7 +885,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
|
||||||
@ -903,7 +903,7 @@ def test_rpc_forceexit(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'id': trade.orders[0].order_id
|
'id': trade.orders[0].order_id
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
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
|
||||||
@ -1195,8 +1195,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(
|
||||||
@ -1234,7 +1234,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'
|
||||||
@ -1243,12 +1243,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'
|
||||||
trade = rpc._rpc_force_entry(pair, None)
|
with pytest.raises(RPCException, match=r"Failed to enter position for TKN/BTC."):
|
||||||
assert trade is None
|
trade = rpc._rpc_force_entry(pair, 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())
|
||||||
|
|
||||||
@ -1260,19 +1260,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)
|
||||||
|
@ -1077,16 +1077,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)
|
||||||
|
@ -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'], "
|
||||||
@ -1047,7 +1047,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
# /forcesell 1
|
# /forcesell 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]
|
||||||
@ -1117,7 +1117,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
|
|||||||
# /forcesell 1
|
# /forcesell 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
|
||||||
|
|
||||||
@ -1178,7 +1178,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
|||||||
# /forcesell all
|
# /forcesell 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
|
||||||
@ -1225,7 +1225,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
# /forcesell 1
|
# /forcesell 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]
|
||||||
|
|
||||||
@ -1234,7 +1234,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = []
|
context.args = []
|
||||||
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 "You must specify a trade-id or 'all'." in msg_mock.call_args_list[0][0][0]
|
assert "You must specify a trade-id or 'all'." in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
@ -1244,12 +1244,12 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
# /forcesell 123456
|
# /forcesell 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_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)
|
||||||
@ -1261,7 +1261,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'
|
||||||
@ -1274,7 +1274,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'
|
||||||
@ -1282,20 +1282,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)
|
||||||
@ -1307,7 +1307,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
|
||||||
@ -1319,7 +1319,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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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})
|
||||||
|
@ -160,7 +160,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 +191,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'])
|
||||||
|
|
||||||
@ -214,7 +214,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 +772,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 +868,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 +977,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 +1238,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),
|
||||||
])
|
])
|
||||||
|
@ -24,7 +24,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)
|
||||||
|
|
||||||
@ -304,7 +304,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
|
||||||
@ -342,7 +342,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,
|
||||||
@ -538,8 +538,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)
|
||||||
@ -752,8 +752,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
|
||||||
@ -976,7 +976,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,
|
||||||
@ -987,11 +987,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)
|
||||||
|
|
||||||
@ -1011,7 +1011,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)),
|
||||||
@ -1033,7 +1033,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=[])
|
||||||
@ -1063,7 +1063,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)
|
||||||
@ -1219,7 +1219,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
|||||||
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)
|
||||||
@ -1262,7 +1262,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)
|
||||||
@ -1327,7 +1327,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,
|
||||||
@ -1366,7 +1366,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)
|
||||||
@ -1487,7 +1487,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})
|
||||||
@ -1595,7 +1595,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})
|
||||||
@ -1862,10 +1862,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
|
||||||
@ -1888,7 +1888,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()
|
||||||
@ -1911,7 +1911,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)
|
||||||
@ -1932,7 +1932,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,
|
||||||
|
|
||||||
@ -1982,7 +1982,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
|
||||||
@ -2008,7 +2008,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,
|
||||||
@ -2028,7 +2028,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)
|
||||||
|
|
||||||
@ -2109,7 +2109,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)
|
||||||
@ -2136,7 +2136,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()
|
||||||
@ -2237,7 +2237,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)
|
||||||
|
|
||||||
@ -2275,14 +2275,14 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee,
|
|||||||
|
|
||||||
|
|
||||||
@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(
|
||||||
@ -2322,7 +2322,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(
|
||||||
@ -2772,7 +2772,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
|
||||||
|
|
||||||
|
|
||||||
@ -2859,8 +2859,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']
|
||||||
|
|
||||||
@ -2874,7 +2874,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']
|
||||||
@ -2902,7 +2902,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])
|
||||||
@ -2921,11 +2921,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
|
||||||
)
|
)
|
||||||
@ -2943,7 +2943,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',
|
||||||
@ -2955,9 +2955,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']
|
||||||
@ -3654,12 +3654,12 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
|
|||||||
(False, 0.10, 0.22, True, False, ExitType.EXIT_SIGNAL.value, False),
|
(False, 0.10, 0.22, True, False, ExitType.EXIT_SIGNAL.value, False),
|
||||||
(False, 0.10, 0.22, True, False, ExitType.EXIT_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, exit_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={
|
||||||
@ -3674,9 +3674,9 @@ 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)
|
||||||
@ -3696,7 +3696,7 @@ def test_sell_profit_only(
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
@ -3816,11 +3816,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={
|
||||||
@ -3834,7 +3834,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)
|
||||||
@ -3880,7 +3880,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,
|
||||||
@ -3938,10 +3938,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={
|
||||||
@ -4033,11 +4033,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={
|
||||||
@ -4054,7 +4054,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)
|
||||||
@ -4441,7 +4441,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -4466,7 +4466,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]
|
||||||
@ -4657,7 +4657,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)],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -4770,7 +4770,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:
|
||||||
@ -5027,7 +5027,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)
|
||||||
|
@ -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
|
||||||
|
@ -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 = enter_order['id']
|
trade.open_order_id = enter_order['id']
|
||||||
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=.*\).",
|
||||||
@ -2137,19 +2137,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
|
||||||
@ -2157,15 +2157,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
|
||||||
@ -2389,7 +2389,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(
|
||||||
@ -2405,16 +2405,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,
|
||||||
@ -2435,13 +2435,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,
|
||||||
@ -2463,13 +2463,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,
|
||||||
@ -2490,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
|
||||||
|
|
||||||
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,
|
||||||
@ -2545,13 +2545,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
2
tests/testdata/backtest-result_new.json
vendored
2
tests/testdata/backtest-result_new.json
vendored
File diff suppressed because one or more lines are too long
10
tests/testdata/strategy_SampleStrategy.fthypt
vendored
10
tests/testdata/strategy_SampleStrategy.fthypt
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user