Compare commits

..

1 Commits

71 changed files with 13131 additions and 14253 deletions

View File

@@ -351,7 +351,7 @@ jobs:
python setup.py sdist bdist_wheel
- name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.5.0
uses: pypa/gh-action-pypi-publish@master
if: (github.event_name == 'release')
with:
user: __token__
@@ -359,7 +359,7 @@ jobs:
repository_url: https://test.pypi.org/legacy/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.5.0
uses: pypa/gh-action-pypi-publish@master
if: (github.event_name == 'release')
with:
user: __token__

2
.gitignore vendored
View File

@@ -80,8 +80,6 @@ instance/
# Sphinx documentation
docs/_build/
# Mkdocs documentation
site/
# PyBuilder
target/

View File

@@ -13,11 +13,11 @@ repos:
- id: mypy
exclude: build_helpers
additional_dependencies:
- types-cachetools==5.2.1
- types-cachetools==5.0.2
- types-filelock==3.2.7
- types-requests==2.28.3
- types-tabulate==0.8.11
- types-python-dateutil==2.8.19
- types-requests==2.27.30
- types-tabulate==0.8.9
- types-python-dateutil==2.8.17
# stages: [push]
- repo: https://github.com/pycqa/isort

View File

@@ -155,8 +155,7 @@
"entry_cancel": "on",
"exit_cancel": "on",
"protection_trigger": "off",
"protection_trigger_global": "on",
"show_candle": "off"
"protection_trigger_global": "on"
},
"reload": true,
"balance_dust_level": 0.01

View File

@@ -20,9 +20,7 @@ All profit calculations of Freqtrade include fees. For Backtesting / Hyperopt /
## Bot execution logic
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
This will also run the `bot_start()` callback.
By default, the bot loop runs every few seconds (`internals.process_throttle_secs`) and performs the following actions:
By default, loop runs every few seconds (`internals.process_throttle_secs`) and does roughly the following in the following sequence:
* Fetch open trades from persistence.
* Calculate current list of tradable pairs.
@@ -56,7 +54,6 @@ This loop will be repeated again and again until the bot is stopped.
[backtesting](backtesting.md) or [hyperopt](hyperopt.md) do only part of the above logic, since most of the trading operations are fully simulated.
* Load historic data for configured pairlist.
* Calls `bot_start()` once.
* Calls `bot_loop_start()` once.
* Calculate indicators (calls `populate_indicators()` once per pair).
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).

View File

@@ -116,9 +116,6 @@ This is similar to using multiple `--config` parameters, but simpler in usage as
The table below will list all configuration parameters available.
Freqtrade can also load many options via command line (CLI) arguments (check out the commands `--help` output for details).
### Configuration option prevalence
The prevalence for all Options is as follows:
- CLI arguments override any other option
@@ -126,8 +123,6 @@ The prevalence for all Options is as follows:
- Configuration files are used in sequence (the last file wins) and override Strategy configurations.
- Strategy configurations are only used if they are not set via configuration or command-line arguments. These options are marked with [Strategy Override](#parameters-in-the-strategy) in the below table.
### Parameters table
Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways.
| Parameter | Description |
@@ -140,7 +135,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
| `timeframe` | The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). Usually missing in configuration, and specified in the strategy. [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
| `timeframe` | The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
@@ -153,16 +148,13 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates) <br>*Defaults to None.*<br> **Datatype:** Float
| `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md). <br>*Defaults to `"spot"`.* <br> **Datatype:** String
| `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md). <br> **Datatype:** String
| `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md). <br>*Defaults to `0.05`.* <br> **Datatype:** Float
| | **Unfilled timeout**
| `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.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 exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
| | **Pricing**
| `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.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
@@ -173,8 +165,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `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.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
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
| | **TODO**
| `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
| `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
| `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)
@@ -182,9 +172,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `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_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
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
| `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1
| | **Exchange**
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
| `recursive_strategy_search` | Set to `true` to recursively search sub-directories inside `user_data/strategies` for a strategy. <br> **Datatype:** Boolean
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.<br> **Datatype:** Boolean
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
@@ -201,19 +190,14 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`<br> **Datatype:** Boolean
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| | **Plugins**
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
| `protections` | Define one or more protections to be used. [More information](plugins.md#protections). <br> **Datatype:** List of Dicts
| | **Telegram**
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `True`.<br> **Datatype:** boolean
| `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.<br> **Datatype:** dictionary
| | **Webhook**
| `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean
| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookentry` | Payload to send on entry. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
@@ -223,7 +207,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `webhook.webhookexitcancel` | Payload to send on exit order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookexitfill` | Payload to send on exit order filled. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| | **Rest API / FreqUI**
| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** Boolean
| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** IPv4
| `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details. <br>**Datatype:** Integer between 1024 and 65535
@@ -231,22 +214,23 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. <br>**Keep it in secret, do not disclose publicly.**<br> **Datatype:** String
| `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. <br>**Keep it in secret, do not disclose publicly.**<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
| | **Other**
| `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`
| `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
| `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
| `internals.process_throttle_secs` | Set the process throttle, or minimum loop duration for one bot iteration loop. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Integer
| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages. <br>*Defaults to `60` seconds.* <br> **Datatype:** Positive Integer or 0
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
| `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
| `recursive_strategy_search` | Set to `true` to recursively search sub-directories inside `user_data/strategies` for a strategy. <br> **Datatype:** Boolean
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <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
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String
| `add_config_files` | Additional config files. These files will be loaded and merged with the current config file. The files are resolved relative to the initial file.<br> *Defaults to `[]`*. <br> **Datatype:** List of strings
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
| `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1
| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates) <br>*Defaults to None.*<br> **Datatype:** Float
### Parameters in the strategy

View File

@@ -334,7 +334,7 @@ lev_tiers = exchange.fetch_leverage_tiers()
# Assumes this is running in the root of the repository.
file = Path('freqtrade/exchange/binance_leverage_tiers.json')
json.dump(dict(sorted(lev_tiers.items())), file.open('w'), indent=2)
json.dump(lev_tiers, file.open('w'), indent=2)
```

View File

@@ -40,15 +40,13 @@ pip install -r requirements-hyperopt.txt
```
usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[--recursive-strategy-search] [-i TIMEFRAME]
[--timerange TIMERANGE]
[-i TIMEFRAME] [--timerange TIMERANGE]
[--data-format-ohlcv {json,jsongz,hdf5}]
[--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[-p PAIRS [PAIRS ...]] [--hyperopt-path PATH]
[--eps] [--dmmp] [--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET]
[--timeframe-detail TIMEFRAME_DETAIL] [-e INT]
[--dry-run-wallet DRY_RUN_WALLET] [-e INT]
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]]
[--print-all] [--no-color] [--print-json] [-j JOBS]
[--random-state INT] [--min-trades INT]
@@ -91,9 +89,6 @@ optional arguments:
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
Starting balance, used for backtesting / hyperopt and
dry-runs.
--timeframe-detail TIMEFRAME_DETAIL
Specify detail timeframe for backtesting (`1m`, `5m`,
`30m`, `1h`, `1d`).
-e INT, --epochs INT Specify number of epochs (default: 100).
--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]
Specify which parameters to hyperopt. Space-separated
@@ -151,9 +146,7 @@ Strategy arguments:
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
--recursive-strategy-search
Recursively search for a strategy in the strategies
folder.
```
### Hyperopt checklist
@@ -278,8 +271,7 @@ The last one we call `trigger` and use it to decide which buy trigger we want to
!!! Note "Parameter space assignment"
Parameters must either be assigned to a variable named `buy_*` or `sell_*` - or contain `space='buy'` | `space='sell'` to be assigned to a space correctly.
If no parameter is available for a space, you'll receive the error that no space was found when running hyperopt.
Parameters with unclear space (e.g. `adx_period = IntParameter(4, 24, default=14)` - no explicit nor implicit space) will not be detected and will therefore be ignored.
If no parameter is available for a space, you'll receive the error that no space was found when running hyperopt.
So let's write the buy strategy using these values:
@@ -342,7 +334,6 @@ There are four parameter types each suited for different purposes.
## Optimizing an indicator parameter
Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy.
By default, we assume a stoploss of 5% - and a take-profit (`minimal_roi`) of 10% - which means freqtrade will sell the trade once 10% profit has been reached.
``` python
from pandas import DataFrame
@@ -357,9 +348,6 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
class MyAwesomeStrategy(IStrategy):
stoploss = -0.05
timeframe = '15m'
minimal_roi = {
"0": 0.10
},
# Define the parameter spaces
buy_ema_short = IntParameter(3, 50, default=5)
buy_ema_long = IntParameter(15, 200, default=50)
@@ -394,7 +382,7 @@ class MyAwesomeStrategy(IStrategy):
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
conditions = []
conditions.append(qtpylib.crossed_above(
dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}']
))
@@ -415,7 +403,7 @@ Using `self.buy_ema_short.range` will return a range object containing all entri
In this case (`IntParameter(3, 50, default=5)`), the loop would run for all numbers between 3 and 50 (`[3, 4, 5, ... 49, 50]`).
By using this in a loop, hyperopt will generate 48 new columns (`['buy_ema_3', 'buy_ema_4', ... , 'buy_ema_50']`).
Hyperopt itself will then use the selected value to create the buy and sell signals.
Hyperopt itself will then use the selected value to create the buy and sell signals
While this strategy is most likely too simple to provide consistent profit, it should serve as an example how optimize indicator parameters.
@@ -874,28 +862,10 @@ You can also enable position stacking in the configuration file by explicitly se
As hyperopt consumes a lot of memory (the complete data needs to be in memory once per parallel backtesting process), it's likely that you run into "out of memory" errors.
To combat these, you have multiple options:
* Reduce the amount of pairs.
* Reduce the timerange used (`--timerange <timerange>`).
* Avoid using `--timeframe-detail` (this loads a lot of additional data into memory).
* Reduce the number of parallel processes (`-j <n>`).
* Increase the memory of your machine.
## The objective has been evaluated at this point before.
If you see `The objective has been evaluated at this point before.` - then this is a sign that your space has been exhausted, or is close to that.
Basically all points in your space have been hit (or a local minima has been hit) - and hyperopt does no longer find points in the multi-dimensional space it did not try yet.
Freqtrade tries to counter the "local minima" problem by using new, randomized points in this case.
Example:
``` python
buy_ema_short = IntParameter(5, 20, default=10, space="buy", optimize=True)
# This is the only parameter in the buy space
```
The `buy_ema_short` space has 15 possible values (`5, 6, ... 19, 20`). If you now run hyperopt for the buy space, hyperopt will only have 15 values to try before running out of options.
Your epochs should therefore be aligned to the possible values - or you should be ready to interrupt a run if you norice a lot of `The objective has been evaluated at this point before.` warnings.
* reduce the amount of pairs
* reduce the timerange used (`--timerange <timerange>`)
* reduce the number of parallel processes (`-j <n>`)
* Increase the memory of your machine
## Show details of Hyperopt results

View File

@@ -50,8 +50,6 @@ This applies across all pairs, unless `only_per_pair` is set to true, which will
Similarly, this protection will by default look at all trades (long and short). For futures bots, setting `only_per_side` will make the bot only consider one side, and will then only lock this one side, allowing for example shorts to continue after a series of long stoplosses.
`required_profit` will determine the required relative profit (or loss) for stoplosses to consider. This should normally not be set and defaults to 0.0 - which means all losing stoplosses will be triggering a block.
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.
``` python
@@ -63,7 +61,6 @@ def protections(self):
"lookback_period_candles": 24,
"trade_limit": 4,
"stop_duration_candles": 4,
"required_profit": 0.0,
"only_per_pair": False,
"only_per_side": False
}

View File

@@ -1,6 +1,5 @@
markdown==3.3.7
mkdocs==1.3.1
mkdocs-material==8.3.9
mdx_truly_sane_lists==1.3
mkdocs==1.3.0
mkdocs-material==8.3.6
mdx_truly_sane_lists==1.2
pymdown-extensions==9.5
jinja2==3.1.2

View File

@@ -130,7 +130,7 @@ In summary: The stoploss will be adjusted to be always be -10% of the highest ob
### Trailing stop loss, custom positive loss
You could also have a default stop loss when you are in the red with your buy (buy - fee), but once you hit a positive result (or an offset you define) the system will utilize a new stop loss, which can have a different value.
It is also possible to have a default stop loss, when you are in the red with your buy (buy - fee), but once you hit positive result the system will utilize a new stop loss, which can have a different value.
For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used.
!!! Note
@@ -142,8 +142,6 @@ Both values require `trailing_stop` to be set to true and `trailing_stop_positiv
stoploss = -0.10
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.0
trailing_only_offset_is_reached = False # Default - not necessary for this example
```
For example, simplified math:
@@ -158,31 +156,11 @@ For example, simplified math:
The 0.02 would translate to a -2% stop loss.
Before this, `stoploss` is used for the trailing stoploss.
!!! Tip "Use an offset to change your stoploss"
Use `trailing_stop_positive_offset` to ensure that your new trailing stoploss will be in profit by setting `trailing_stop_positive_offset` higher than `trailing_stop_positive`. Your first new stoploss value will then already have locked in profits.
Example with simplified math:
``` python
stoploss = -0.10
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
```
* the bot buys an asset at a price of 100$
* the stop loss is defined at -10%, so the stop loss would get triggered once the asset drops below 90$
* assuming the asset now increases to 102$
* the stoploss will now be at 91.8$ - 10% below the highest observed rate
* assuming the asset now increases to 103.5$ (above the offset configured)
* the stop loss will now be -2% of 103.5$ = 101.43$
* now the asset drops in value to 102\$, the stop loss will still be 101.43$ and would trigger once price breaks below 101.43$
### Trailing stop loss only once the trade has reached a certain offset
You can also keep a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns.
It is also possible to use a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns.
If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`.
If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`.
This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset.
``` python
@@ -225,6 +203,7 @@ If price moves 1% - you've lost 10$ of your own capital - therfore stoploss will
Make sure to be aware of this, and avoid using too tight stoploss (at 10x leverage, 10% risk may be too little to allow the trade to "breath" a little).
## Changing stoploss on open trades
A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works).

View File

@@ -224,5 +224,3 @@ for val in self.buy_ema_short.range:
# Append columns to existing dataframe
merged_frame = pd.concat(frames, axis=1)
```
Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existant.

View File

@@ -82,9 +82,8 @@ Called before entering a trade, makes it possible to manage your position size w
```python
class AwesomeStrategy(IStrategy):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: Optional[float], max_stake: float,
leverage: float, entry_tag: Optional[str], side: str,
**kwargs) -> float:
proposed_stake: float, min_stake: float, max_stake: float,
entry_tag: Optional[str], side: str, **kwargs) -> float:
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
current_candle = dataframe.iloc[-1].squeeze()
@@ -674,10 +673,9 @@ class DigDeeperStrategy(IStrategy):
max_dca_multiplier = 5.5
# This is called when placing the initial order (opening trade)
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: Optional[float], max_stake: float,
leverage: float, entry_tag: Optional[str], side: str,
**kwargs) -> float:
entry_tag: Optional[str], side: str, **kwargs) -> float:
# We need to leave most of the funds for possible further DCA orders
# This also applies to fixed stakes

View File

@@ -646,9 +646,6 @@ This is where calling `self.dp.current_whitelist()` comes in handy.
return informative_pairs
```
??? Note "Plotting with current_whitelist"
Current whitelist is not supported for `plot-dataframe`, as this command is usually used by providing an explicit pairlist - and would therefore make the return values of this method misleading.
### *get_pair_dataframe(pair, timeframe)*
``` python

View File

@@ -31,13 +31,11 @@ pair = "BTC/USDT"
```python
# Load data using values set above
from freqtrade.data.history import load_pair_history
from freqtrade.enums import CandleType
candles = load_pair_history(datadir=data_location,
timeframe=config["timeframe"],
pair=pair,
data_format = "hdf5",
candle_type=CandleType.SPOT,
)
# Confirm success
@@ -95,7 +93,7 @@ from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats
# if backtest_dir points to a directory, it'll automatically load the last backtest file.
backtest_dir = config["user_data_dir"] / "backtest_results"
# backtest_dir can also point to a specific file
# backtest_dir can also point to a specific file
# backtest_dir = config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json"
```

View File

@@ -97,8 +97,7 @@ Example configuration showing the different settings:
"entry_fill": "off",
"exit_fill": "off",
"protection_trigger": "off",
"protection_trigger_global": "on",
"show_candle": "off"
"protection_trigger_global": "on"
},
"reload": true,
"balance_dust_level": 0.01
@@ -109,7 +108,7 @@ Example configuration showing the different settings:
`exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
`*_fill` notifications are off by default and must be explicitly enabled.
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
`show_candle` - show candle values as part of entry/exit messages. Only possible value is "ohlc".
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
`reload` allows you to disable reload-buttons on selected messages.

View File

@@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = '2022.7'
__version__ = 'develop'
if 'dev' in __version__:
try:

View File

@@ -28,7 +28,7 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
"position_stacking", "use_max_market_positions",
"enable_protections", "dry_run_wallet", "timeframe_detail",
"enable_protections", "dry_run_wallet",
"epochs", "spaces", "print_all",
"print_colorized", "print_json", "hyperopt_jobs",
"hyperopt_random_state", "hyperopt_min_trades",

View File

@@ -24,7 +24,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
print_colorized = config.get('print_colorized', False)
print_json = config.get('print_json', False)
export_csv = config.get('export_csv')
export_csv = config.get('export_csv', None)
no_details = config.get('hyperopt_list_no_details', False)
no_header = False

View File

@@ -129,7 +129,7 @@ class Configuration:
# Default to in-memory db for dry_run if not specified
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
else:
if not config.get('db_url'):
if not config.get('db_url', None):
config['db_url'] = constants.DEFAULT_DB_PROD_URL
logger.info('Dry run is disabled')
@@ -182,7 +182,7 @@ class Configuration:
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False)
logger.info('Using user-data directory: %s ...', config['user_data_dir'])
config.update({'datadir': create_datadir(config, self.args.get('datadir'))})
config.update({'datadir': create_datadir(config, self.args.get('datadir', None))})
logger.info('Using data directory: %s ...', config.get('datadir'))
if self.args.get('exportfilename'):
@@ -221,7 +221,7 @@ class Configuration:
if config.get('max_open_trades') == -1:
config['max_open_trades'] = float('inf')
if self.args.get('stake_amount'):
if self.args.get('stake_amount', None):
# Convert explicitly to float to support CLI argument for both unlimited and value
try:
self.args['stake_amount'] = float(self.args['stake_amount'])
@@ -474,7 +474,7 @@ class Configuration:
configuration instead of the content)
"""
if (argname in self.args and self.args[argname] is not None
and self.args[argname] is not False):
and self.args[argname] is not False):
config.update({argname: self.args[argname]})
if logfun:

View File

@@ -313,10 +313,6 @@ CONF_SCHEMA = {
'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS,
},
'show_candle': {
'type': 'string',
'enum': ['off', 'ohlc'],
},
}
},
'reload': {'type': 'boolean'},
@@ -542,4 +538,3 @@ TradeList = List[List]
LongShort = Literal['long', 'short']
EntryExit = Literal['entry', 'exit']
BuySell = Literal['buy', 'sell']
MakerTaker = Literal['maker', 'taker']

File diff suppressed because it is too large Load Diff

View File

@@ -46,7 +46,6 @@ MAP_EXCHANGE_CHILDCLASS = {
'binanceje': 'binance',
'binanceusdm': 'binance',
'okex': 'okx',
'gate': 'gateio',
}
SUPPORTED_EXCHANGES = [
@@ -64,16 +63,17 @@ EXCHANGE_HAS_REQUIRED = [
'fetchOrder',
'cancelOrder',
'createOrder',
# 'createLimitOrder', 'createMarketOrder',
'fetchBalance',
# Public endpoints
'loadMarkets',
'fetchOHLCV',
]
EXCHANGE_HAS_OPTIONAL = [
# Private
'fetchMyTrades', # Trades for order - fee detection
'createLimitOrder', 'createMarketOrder', # Either OR for orders
# 'setLeverage', # Margin/Futures trading
# 'setMarginMode', # Margin/Futures trading
# 'fetchFundingHistory', # Futures trading

View File

@@ -20,7 +20,7 @@ from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, Precise, decimal_to_
from pandas import DataFrame
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell,
EntryExit, ListPairsWithTimeframes, MakerTaker, PairWithTimeframe)
EntryExit, ListPairsWithTimeframes, PairWithTimeframe)
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
@@ -77,9 +77,7 @@ class Exchange:
"mark_ohlcv_price": "mark",
"mark_ohlcv_timeframe": "8h",
"ccxt_futures_name": "swap",
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
}
_ft_has: Dict = {}
_ft_has_futures: Dict = {}
@@ -88,8 +86,7 @@ class Exchange:
# TradingMode.SPOT always supported and not required in this list
]
def __init__(self, config: Dict[str, Any], validate: bool = True,
load_leverage_tiers: bool = False) -> None:
def __init__(self, config: Dict[str, Any], validate: bool = True) -> None:
"""
Initializes this module with the given config,
it does basic validation whether the specified exchange and pairs are valid.
@@ -177,17 +174,29 @@ class Exchange:
logger.info(f'Using Exchange "{self.name}"')
if validate:
# Check if timeframe is available
self.validate_timeframes(config.get('timeframe'))
# Initial markets load
self._load_markets()
self.validate_config(config)
# Check if all pairs are available
self.validate_stakecurrency(config['stake_currency'])
if not exchange_config.get('skip_pair_validation'):
self.validate_pairs(config['exchange']['pair_whitelist'])
self.validate_ordertypes(config.get('order_types', {}))
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
self.required_candle_call_count = self.validate_required_startup_candles(
config.get('startup_candle_count', 0), config.get('timeframe', ''))
self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
self.validate_pricing(config['exit_pricing'])
self.validate_pricing(config['entry_pricing'])
# Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = exchange_config.get(
"markets_refresh_interval", 60) * 60
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
if self.trading_mode != TradingMode.SPOT:
self.fill_leverage_tiers()
self.additional_exchange_init()
@@ -204,20 +213,6 @@ class Exchange:
logger.info("Closing async ccxt session.")
self.loop.run_until_complete(self._api_async.close())
def validate_config(self, config):
# Check if timeframe is available
self.validate_timeframes(config.get('timeframe'))
# Check if all pairs are available
self.validate_stakecurrency(config['stake_currency'])
if not config['exchange'].get('skip_pair_validation'):
self.validate_pairs(config['exchange']['pair_whitelist'])
self.validate_ordertypes(config.get('order_types', {}))
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
self.validate_pricing(config['exit_pricing'])
self.validate_pricing(config['entry_pricing'])
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
ccxt_kwargs: Dict = {}) -> ccxt.Exchange:
"""
@@ -392,7 +387,7 @@ class Exchange:
and market.get('base', None) is not None
and (self.precisionMode != TICK_SIZE
# Too low precision will falsify calculations
or market.get('precision', {}).get('price') > 1e-11)
or market.get('precision', {}).get('price', None) > 1e-11)
and ((self.trading_mode == TradingMode.SPOT and self.market_is_spot(market))
or (self.trading_mode == TradingMode.MARGIN and self.market_is_margin(market))
or (self.trading_mode == TradingMode.FUTURES and self.market_is_future(market)))
@@ -427,7 +422,7 @@ class Exchange:
if 'symbol' in order and order['symbol'] is not None:
contract_size = self._get_contract_size(order['symbol'])
if contract_size != 1:
for prop in self._ft_has.get('order_props_in_contracts', []):
for prop in ['amount', 'cost', 'filled', 'remaining']:
if prop in order and order[prop] is not None:
order[prop] = order[prop] * contract_size
return order
@@ -542,7 +537,7 @@ class Exchange:
# The internal info array is different for each particular market,
# its contents depend on the exchange.
# It can also be a string or similar ... so we need to verify that first.
elif (isinstance(self.markets[pair].get('info'), dict)
elif (isinstance(self.markets[pair].get('info', None), dict)
and self.markets[pair].get('info', {}).get('prohibitedIn', False)):
# Warn users about restricted pairs in whitelist.
# We cannot determine reliably if Users are affected.
@@ -825,7 +820,7 @@ class Exchange:
'price': rate,
'average': rate,
'amount': _amount,
'cost': _amount * rate,
'cost': _amount * rate / leverage,
'type': ordertype,
'side': side,
'filled': 0,
@@ -851,27 +846,20 @@ class Exchange:
'filled': _amount,
'cost': (dry_order['amount'] * average) / leverage
})
# market orders will always incurr taker fees
dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
dry_order = self.add_dry_order_fee(pair, dry_order)
dry_order = self.check_dry_limit_order_filled(dry_order, immediate=True)
dry_order = self.check_dry_limit_order_filled(dry_order)
self._dry_run_open_orders[dry_order["id"]] = dry_order
# Copy order and close it - so the returned order is open unless it's a market order
return dry_order
def add_dry_order_fee(
self,
pair: str,
dry_order: Dict[str, Any],
taker_or_maker: MakerTaker,
) -> Dict[str, Any]:
fee = self.get_fee(pair, taker_or_maker=taker_or_maker)
def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]) -> Dict[str, Any]:
dry_order.update({
'fee': {
'currency': self.get_pair_quote_currency(pair),
'cost': dry_order['cost'] * fee,
'rate': fee
'cost': dry_order['cost'] * self.get_fee(pair),
'rate': self.get_fee(pair)
}
})
return dry_order
@@ -937,8 +925,7 @@ class Exchange:
pass
return False
def check_dry_limit_order_filled(
self, order: Dict[str, Any], immediate: bool = False) -> Dict[str, Any]:
def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]:
"""
Check dry-run limit order fill and update fee (if it filled).
"""
@@ -952,12 +939,7 @@ class Exchange:
'filled': order['amount'],
'remaining': 0,
})
self.add_dry_order_fee(
pair,
order,
'taker' if immediate else 'maker',
)
self.add_dry_order_fee(pair, order)
return order
@@ -1264,7 +1246,7 @@ class Exchange:
return False
required = ('fee', 'status', 'amount')
return all(corder.get(k, None) is not None for k in required)
return all(k in corder for k in required)
def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
"""
@@ -1615,7 +1597,7 @@ class Exchange:
@retrier
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
price: float = 1, taker_or_maker: MakerTaker = 'maker') -> float:
price: float = 1, taker_or_maker: str = 'maker') -> float:
try:
if self._config['dry_run'] and self._config.get('fee', None) is not None:
return self._config['fee']
@@ -1649,35 +1631,27 @@ class Exchange:
and order['fee']['cost'] is not None
)
def calculate_fee_rate(
self, fee: Dict, symbol: str, cost: float, amount: float) -> Optional[float]:
def calculate_fee_rate(self, order: Dict) -> Optional[float]:
"""
Calculate fee rate if it's not given by the exchange.
:param fee: ccxt Fee dict - must contain cost / currency / rate
:param symbol: Symbol of the order
:param cost: Total cost of the order
:param amount: Amount of the order
:param order: Order or trade (one trade) dict
"""
if fee.get('rate') is not None:
return fee.get('rate')
fee_curr = fee.get('currency')
if fee_curr is None:
return None
fee_cost = float(fee['cost'])
if self._ft_has['fee_cost_in_contracts']:
# Convert cost via "contracts" conversion
fee_cost = self._contracts_to_amount(symbol, fee['cost'])
if order['fee'].get('rate') is not None:
return order['fee'].get('rate')
fee_curr = order['fee']['currency']
# Calculate fee based on order details
if fee_curr == self.get_pair_base_currency(symbol):
if fee_curr in self.get_pair_base_currency(order['symbol']):
# Base currency - divide by amount
return round(fee_cost / amount, 8)
elif fee_curr == self.get_pair_quote_currency(symbol):
return round(
order['fee']['cost'] / safe_value_fallback2(order, order, 'filled', 'amount'), 8)
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
# Quote currency - divide by cost
return round(fee_cost / cost, 8) if cost else None
return round(self._contracts_to_amount(
order['symbol'], order['fee']['cost']) / order['cost'],
8) if order['cost'] else None
else:
# If Fee currency is a different currency
if not cost:
if not order['cost']:
# If cost is None or 0.0 -> falsy, return None
return None
try:
@@ -1689,28 +1663,19 @@ class Exchange:
fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
if not fee_to_quote_rate:
return None
return round((fee_cost * fee_to_quote_rate) / cost, 8)
return round((self._contracts_to_amount(
order['symbol'], order['fee']['cost']) * fee_to_quote_rate) / order['cost'], 8)
def extract_cost_curr_rate(self, fee: Dict, symbol: str, cost: float,
amount: float) -> Tuple[float, str, Optional[float]]:
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
"""
Extract tuple of cost, currency, rate.
Requires order_has_fee to run first!
:param fee: ccxt Fee dict - must contain cost / currency / rate
:param symbol: Symbol of the order
:param cost: Total cost of the order
:param amount: Amount of the order
:param order: Order or trade (one trade) dict
:return: Tuple with cost, currency, rate of the given fee dict
"""
return (float(fee['cost']),
fee['currency'],
self.calculate_fee_rate(
fee,
symbol,
cost,
amount
)
)
return (order['fee']['cost'],
order['fee']['currency'],
self.calculate_fee_rate(order))
# Historic data

View File

@@ -1,13 +1,12 @@
""" Gate.io exchange subclass """
import logging
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from typing import Dict, List, Optional, Tuple
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange
from freqtrade.misc import safe_value_fallback2
logger = logging.getLogger(__name__)
@@ -33,9 +32,7 @@ class Gateio(Exchange):
}
_ft_has_futures: Dict = {
"needs_trading_fees": True,
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
"needs_trading_fees": True
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
@@ -98,29 +95,12 @@ class Gateio(Exchange):
}
return trades
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
if self.trading_mode == TradingMode.FUTURES:
return safe_value_fallback2(order, order, 'id_stop', 'id')
return order['id']
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
order = self.fetch_order(
return self.fetch_order(
order_id=order_id,
pair=pair,
params={'stop': True}
)
if self.trading_mode == TradingMode.FUTURES:
if order['status'] == 'closed':
# Places a real order - which we need to fetch explicitly.
new_orderid = order.get('info', {}).get('trade_id')
if new_orderid:
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
order1['id_stop'] = order1['id']
order1['id'] = order_id
order1['stopPrice'] = order.get('stopPrice')
return order1
return order
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
return self.cancel_order(

View File

@@ -28,7 +28,6 @@ class Okx(Exchange):
}
_ft_has_futures: Dict = {
"tickers_have_quoteVolume": False,
"fee_cost_in_contracts": True,
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [

View File

@@ -17,12 +17,13 @@ from freqtrade.constants import BuySell, LongShort
from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge
from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode, SignalDirection,
State, TradingMode)
from freqtrade.enums import (ExitCheckTuple, ExitType, MarginMode, RPCMessageType, RunMode,
SignalDirection, State, TradingMode)
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.exchange.exchange import timeframe_to_next_date
from freqtrade.maintenance_margin import MaintenanceMargin
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin
from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
@@ -65,10 +66,9 @@ class FreqtradeBot(LoggingMixin):
# Check config consistency here since strategies can set certain options
validate_config_consistency(config)
self.exchange = ExchangeResolver.load_exchange(
self.config['exchange']['name'], self.config, load_leverage_tiers=True)
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
init_db(self.config['db_url'])
init_db(self.config.get('db_url', None))
self.wallets = Wallets(self.config, self.exchange)
@@ -105,7 +105,11 @@ class FreqtradeBot(LoggingMixin):
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
self.margin_mode: MarginMode = (
MarginMode(config.get('margin_mode'))
if config.get('margin_mode')
else MarginMode.NONE
)
self._schedule = Scheduler()
if self.trading_mode == TradingMode.FUTURES:
@@ -126,6 +130,16 @@ class FreqtradeBot(LoggingMixin):
# Initialize protections AFTER bot start - otherwise parameters are not loaded.
self.protections = ProtectionManager(self.config, self.strategy.protections)
# Start calculating maintenance margin if on cross margin
# TODO-lev: finish the below...
if self.margin_mode == MarginMode.CROSS:
self.maintenance_margin = MaintenanceMargin(
exchange_name=self.exchange.name,
trading_mode=self.trading_mode)
self.maintenance_margin.run()
def notify_status(self, msg: str) -> None:
"""
Public method for users of this class (worker, etc.) to send notifications
@@ -333,8 +347,6 @@ class FreqtradeBot(LoggingMixin):
if not trade.is_open and not trade.fee_updated(trade.exit_side):
# Get sell fee
order = trade.select_order(trade.exit_side, False)
if not order:
order = trade.select_order('stoploss', False)
if order:
logger.info(
f"Updating {trade.exit_side}-fee on trade {trade}"
@@ -637,7 +649,7 @@ class FreqtradeBot(LoggingMixin):
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
entry_tag=enter_tag, side=trade_side):
logger.info(f"User denied entry for {pair}.")
logger.info(f"User requested abortion of buying {pair}")
return False
order = self.exchange.create_order(
pair=pair,
@@ -651,7 +663,7 @@ class FreqtradeBot(LoggingMixin):
)
order_obj = Order.parse_from_ccxt_object(order, pair, side)
order_id = order['id']
order_status = order.get('status')
order_status = order.get('status', None)
logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.")
# we assume the order is executed at the price requested
@@ -724,6 +736,10 @@ class FreqtradeBot(LoggingMixin):
trade.orders.append(order_obj)
trade.recalc_trade_from_orders()
if self.margin_mode == MarginMode.CROSS:
self.maintenance_margin.add_new_trade(trade)
Trade.query.session.add(trade)
Trade.commit()
@@ -817,7 +833,7 @@ class FreqtradeBot(LoggingMixin):
pair=pair, current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested, proposed_stake=stake_amount,
min_stake=min_stake_amount, max_stake=min(max_stake_amount, stake_available),
leverage=leverage, entry_tag=entry_tag, side=trade_side
entry_tag=entry_tag, side=trade_side
)
stake_amount = self.wallets.validate_stake_amount(
@@ -962,29 +978,6 @@ class FreqtradeBot(LoggingMixin):
logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
return False
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
enter: bool, exit_: bool, exit_tag: Optional[str]) -> bool:
"""
Check and execute trade exit
"""
exits: List[ExitCheckTuple] = self.strategy.should_exit(
trade,
exit_rate,
datetime.now(timezone.utc),
enter=enter,
exit_=exit_,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
)
for should_exit in exits:
if should_exit.exit_flag:
exit_tag1 = exit_tag if should_exit.exit_type == ExitType.EXIT_SIGNAL else None
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
f'{f" Tag: {exit_tag1}" if exit_tag1 is not None else ""}')
exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag1)
if exited:
return True
return False
def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
"""
Abstracts creating stoploss orders from the logic.
@@ -1136,6 +1129,28 @@ class FreqtradeBot(LoggingMixin):
logger.warning(f"Could not create trailing stoploss order "
f"for pair {trade.pair}.")
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
enter: bool, exit_: bool, exit_tag: Optional[str]) -> bool:
"""
Check and execute trade exit
"""
exits: List[ExitCheckTuple] = self.strategy.should_exit(
trade,
exit_rate,
datetime.now(timezone.utc),
enter=enter,
exit_=exit_,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
)
for should_exit in exits:
if should_exit.exit_flag:
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
f'{f" Tag: {exit_tag}" if exit_tag is not None else ""}')
exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
if exited:
return True
return False
def manage_open_orders(self) -> None:
"""
Management of open orders on exchange. Unfilled orders might be cancelled if timeout
@@ -1468,7 +1483,7 @@ class FreqtradeBot(LoggingMixin):
time_in_force=time_in_force, exit_reason=exit_reason,
sell_reason=exit_reason, # sellreason -> compatibility
current_time=datetime.now(timezone.utc)):
logger.info(f"User denied exit for {trade.pair}.")
logger.info(f"User requested abortion of {trade.pair} exit.")
return False
try:
@@ -1505,10 +1520,21 @@ class FreqtradeBot(LoggingMixin):
# In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') in ('closed', 'expired'):
self.update_trade_state(trade, trade.open_order_id, order)
Trade.commit()
self._remove_maintenance_trade(trade)
return True
def _remove_maintenance_trade(self, trade: Trade):
"""
Removes a trade from the maintenance margin object
:param trade: The trade to remove from the maintenance margin
"""
if self.margin_mode == MarginMode.CROSS:
self.maintenance_margin.remove_trade(trade)
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None:
"""
Sends rpc notification when a sell occurred.
@@ -1545,7 +1571,7 @@ class FreqtradeBot(LoggingMixin):
'open_date': trade.open_date,
'close_date': trade.close_date or datetime.utcnow(),
'stake_currency': self.config['stake_currency'],
'fiat_currency': self.config.get('fiat_display_currency'),
'fiat_currency': self.config.get('fiat_display_currency', None),
}
if 'fiat_display_currency' in self.config:
@@ -1656,7 +1682,7 @@ class FreqtradeBot(LoggingMixin):
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
# If a entry order was closed, force update on stoploss on exchange
if order.get('side') == trade.entry_side:
if order.get('side', None) == trade.entry_side:
trade = self.cancel_stoploss_on_exchange(trade)
# TODO: Margin will need to use interest_rate as well.
# interest_rate = self.exchange.get_interest_rate()
@@ -1745,8 +1771,7 @@ class FreqtradeBot(LoggingMixin):
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
# use fee from order-dict if possible
if self.exchange.order_has_fee(order):
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(
order['fee'], order['symbol'], order['cost'], order_obj.safe_filled)
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
logger.info(f"Fee for Trade {trade} [{order_obj.ft_order_side}]: "
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
if fee_rate is None or fee_rate < 0.02:
@@ -1784,15 +1809,7 @@ class FreqtradeBot(LoggingMixin):
for exectrade in trades:
amount += exectrade['amount']
if self.exchange.order_has_fee(exectrade):
# Prefer singular fee
fees = [exectrade['fee']]
else:
fees = exectrade.get('fees', [])
for fee in fees:
fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(
fee, exectrade['symbol'], exectrade['cost'], exectrade['amount']
)
fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(exectrade)
fee_cost += fee_cost_
if fee_rate_ is not None:
fee_rate_array.append(fee_rate_)

View File

@@ -0,0 +1,52 @@
from typing import List
from freqtrade.enums import TradingMode
from freqtrade.leverage import liquidation_price
from freqtrade.persistence import Trade
class MaintenanceMargin:
trades: List[Trade]
exchange_name: str
trading_mode: TradingMode
@property
def margin_level(self):
# This is the current value of all assets,
# and if you pass below liq_level, you are liquidated
# TODO-lev: Add args to formula
return liquidation_price(
trading_mode=self.trading_mode,
exchange_name=self.exchange_name
)
@property
def liq_level(self): # This may be a constant value and may not need a function
# TODO-lev: The is the value that you are liquidated at
return # If constant, would need to be recalculated after each new trade
def __init__(self, exchange_name: str, trading_mode: TradingMode):
self.exchange_name = exchange_name
self.trading_mode = trading_mode
return
def add_new_trade(self, trade):
self.trades.append(trade)
def remove_trade(self, trade):
self.trades.remove(trade)
# ? def update_trade_pric(self):
def sell_all(self):
# TODO-lev
return
def run(self):
# TODO-lev: implement a thread that constantly updates with every price change,
# TODO-lev: must update at least every few seconds or so
# while true:
# if self.margin_level <= self.liq_level:
# self.sell_all()
return

View File

@@ -84,11 +84,10 @@ class Backtesting:
self.processed_dfs: Dict[str, Dict] = {}
self._exchange_name = self.config['exchange']['name']
self.exchange = ExchangeResolver.load_exchange(
self._exchange_name, self.config, load_leverage_tiers=True)
self.exchange = ExchangeResolver.load_exchange(self._exchange_name, self.config)
self.dataprovider = DataProvider(self.config, self.exchange)
if self.config.get('strategy_list'):
if self.config.get('strategy_list', None):
for strat in list(self.config['strategy_list']):
stratconf = deepcopy(self.config)
stratconf['strategy'] = strat
@@ -190,7 +189,6 @@ class Backtesting:
self.strategy.order_types['stoploss_on_exchange'] = False
self.strategy.ft_bot_start()
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
def _load_protections(self, strategy: IStrategy):
if self.config.get('enable_protections', False):
@@ -723,7 +721,7 @@ class Backtesting:
pair=pair, current_time=current_time, current_rate=propose_rate,
proposed_stake=stake_amount, min_stake=min_stake_amount,
max_stake=min(stake_available, max_stake_amount),
leverage=leverage, entry_tag=entry_tag, side=direction)
entry_tag=entry_tag, side=direction)
stake_amount_val = self.wallets.validate_stake_amount(
pair=pair,
@@ -1142,6 +1140,8 @@ class Backtesting:
backtest_start_time = datetime.now(timezone.utc)
self._set_strategy(strat)
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True):
# Must come from strategy config, as the strategy may modify this setting.

View File

@@ -6,7 +6,6 @@ This module contains the hyperopt logic
import logging
import random
import sys
import warnings
from datetime import datetime, timezone
from math import ceil
@@ -18,7 +17,6 @@ import rapidjson
from colorama import Fore, Style
from colorama import init as colorama_init
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
from joblib.externals import cloudpickle
from pandas import DataFrame
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
@@ -89,7 +87,6 @@ class Hyperopt:
self.backtesting._set_strategy(self.backtesting.strategylist[0])
self.custom_hyperopt.strategy = self.backtesting.strategy
self.hyperopt_pickle_magic(self.backtesting.strategy.__class__.__bases__)
self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss(
self.config)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
@@ -140,17 +137,6 @@ class Hyperopt:
logger.info(f"Removing `{p}`.")
p.unlink()
def hyperopt_pickle_magic(self, bases) -> None:
"""
Hyperopt magic to allow strategy inheritance across files.
For this to properly work, we need to register the module of the imported class
to pickle as value.
"""
for modules in bases:
if modules.__name__ != 'IStrategy':
cloudpickle.register_pickle_by_value(sys.modules[modules.__module__])
self.hyperopt_pickle_magic(modules.__bases__)
def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict:
# Ensure the number of dimensions match
@@ -469,7 +455,7 @@ class Hyperopt:
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]
def start(self) -> None:
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state'))
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
logger.info(f"Using optimizer random state: {self.random_state}")
self.hyperopt_table_header = -1
# Initialize spaces ...

View File

@@ -127,14 +127,14 @@ class HyperoptTools():
'only_profitable': config.get('hyperopt_list_profitable', False),
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time'),
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time'),
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit'),
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit'),
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit'),
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit'),
'filter_min_objective': config.get('hyperopt_list_min_objective'),
'filter_max_objective': config.get('hyperopt_list_max_objective'),
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
}
if not HyperoptTools._test_hyperopt_results_exist(results_file):
# No file found.

View File

@@ -1,10 +1,9 @@
import logging
from typing import List
from sqlalchemy import inspect, select, text, tuple_, update
from sqlalchemy import inspect, text
from freqtrade.exceptions import OperationalException
from freqtrade.persistence.trade_model import Order, Trade
logger = logging.getLogger(__name__)
@@ -252,31 +251,31 @@ def set_sqlite_to_wal(engine):
def fix_old_dry_orders(engine):
with engine.begin() as connection:
stmt = update(Order).where(
Order.ft_is_open.is_(True),
tuple_(Order.ft_trade_id, Order.order_id).not_in(
select(
Trade.id, Trade.stoploss_order_id
).where(Trade.stoploss_order_id.is_not(None))
),
Order.ft_order_side == 'stoploss',
Order.order_id.like('dry%'),
).values(ft_is_open=False)
connection.execute(stmt)
stmt = update(Order).where(
Order.ft_is_open.is_(True),
tuple_(Order.ft_trade_id, Order.order_id).not_in(
select(
Trade.id, Trade.open_order_id
).where(Trade.open_order_id.is_not(None))
),
Order.ft_order_side != 'stoploss',
Order.order_id.like('dry%')
).values(ft_is_open=False)
connection.execute(stmt)
connection.execute(
text(
"""
update orders
set ft_is_open = 0
where ft_is_open = 1 and (ft_trade_id, order_id) not in (
select id, stoploss_order_id from trades where stoploss_order_id is not null
) and ft_order_side = 'stoploss'
and order_id like 'dry_%'
"""
)
)
connection.execute(
text(
"""
update orders
set ft_is_open = 0
where ft_is_open = 1
and (ft_trade_id, order_id) not in (
select id, open_order_id from trades where open_order_id is not null
) and ft_order_side != 'stoploss'
and order_id like 'dry_%'
"""
)
)
def check_migrate(engine, decl_base, previous_tables) -> None:

View File

@@ -821,7 +821,7 @@ class LocalTrade():
self.open_rate = total_stake / total_amount
self.stake_amount = total_stake / (self.leverage or 1.0)
self.amount = total_amount
self.fee_open_cost = self.fee_open * total_stake
self.fee_open_cost = self.fee_open * self.stake_amount
self.recalc_open_trade_value()
if self.stop_loss_pct is not None and self.open_rate is not None:
self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)

View File

@@ -255,18 +255,18 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
"""
# Trades can be empty
if trades is not None and len(trades) > 0:
# Create description for exit summarizing the trade
# Create description for sell summarizing the trade
trades['desc'] = trades.apply(
lambda row: f"{row['profit_ratio']:.2%}, " +
(f"{row['enter_tag']}, " if row['enter_tag'] is not None else "") +
f"{row['exit_reason']}, " +
f"{row['trade_duration']} min",
axis=1)
trade_entries = go.Scatter(
trade_buys = go.Scatter(
x=trades["open_date"],
y=trades["open_rate"],
mode='markers',
name='Trade entry',
name='Trade buy',
text=trades["desc"],
marker=dict(
symbol='circle-open',
@@ -277,12 +277,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
)
)
trade_exits = go.Scatter(
trade_sells = go.Scatter(
x=trades.loc[trades['profit_ratio'] > 0, "close_date"],
y=trades.loc[trades['profit_ratio'] > 0, "close_rate"],
text=trades.loc[trades['profit_ratio'] > 0, "desc"],
mode='markers',
name='Exit - Profit',
name='Sell - Profit',
marker=dict(
symbol='square-open',
size=11,
@@ -290,12 +290,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
color='green'
)
)
trade_exits_loss = go.Scatter(
trade_sells_loss = go.Scatter(
x=trades.loc[trades['profit_ratio'] <= 0, "close_date"],
y=trades.loc[trades['profit_ratio'] <= 0, "close_rate"],
text=trades.loc[trades['profit_ratio'] <= 0, "desc"],
mode='markers',
name='Exit - Loss',
name='Sell - Loss',
marker=dict(
symbol='square-open',
size=11,
@@ -303,9 +303,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
color='red'
)
)
fig.add_trace(trade_entries, 1, 1)
fig.add_trace(trade_exits, 1, 1)
fig.add_trace(trade_exits_loss, 1, 1)
fig.add_trace(trade_buys, 1, 1)
fig.add_trace(trade_sells, 1, 1)
fig.add_trace(trade_sells_loss, 1, 1)
else:
logger.warning("No trades found.")
return fig
@@ -444,7 +444,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
Generate the graph from the data generated by Backtesting or from DB
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators
:param pair: Pair to Display on the graph
:param data: OHLCV DataFrame containing indicators and entry/exit signals
:param data: OHLCV DataFrame containing indicators and buy/sell signals
:param trades: All trades created
:param indicators1: List containing Main plot indicators
:param indicators2: List containing Sub plot indicators

View File

@@ -30,7 +30,7 @@ class AgeFilter(IPairList):
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
self._max_days_listed = pairlistconfig.get('max_days_listed')
self._max_days_listed = pairlistconfig.get('max_days_listed', None)
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
if self._min_days_listed < 1:

View File

@@ -21,7 +21,7 @@ class PerformanceFilter(IPairList):
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._minutes = pairlistconfig.get('minutes', 0)
self._min_profit = pairlistconfig.get('min_profit')
self._min_profit = pairlistconfig.get('min_profit', None)
@property
def needstickers(self) -> bool:

View File

@@ -27,7 +27,7 @@ class RangeStabilityFilter(IPairList):
self._days = pairlistconfig.get('lookback_days', 10)
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change')
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change', None)
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
self._def_candletype = self._config['candle_type_def']

View File

@@ -28,7 +28,7 @@ class PairListManager(LoggingMixin):
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
self._pairlist_handlers: List[IPairList] = []
self._tickers_needed = False
for pairlist_handler_config in self._config.get('pairlists', []):
for pairlist_handler_config in self._config.get('pairlists', None):
pairlist_handler = PairListResolver.load_pairlist(
pairlist_handler_config['method'],
exchange=exchange,

View File

@@ -23,14 +23,13 @@ class StoplossGuard(IProtection):
self._trade_limit = protection_config.get('trade_limit', 10)
self._disable_global_stop = protection_config.get('only_per_pair', False)
self._only_per_side = protection_config.get('only_per_side', False)
self._profit_limit = protection_config.get('required_profit', 0.0)
def short_desc(self) -> str:
"""
Short method description - used for startup-messages
"""
return (f"{self.name} - Frequent Stoploss Guard, {self._trade_limit} stoplosses "
f"with profit < {self._profit_limit:.2%} within {self.lookback_period_str}.")
f"within {self.lookback_period_str}.")
def _reason(self) -> str:
"""
@@ -50,7 +49,7 @@ class StoplossGuard(IProtection):
trades = [trade for trade in trades1 if (str(trade.exit_reason) in (
ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value,
ExitType.STOPLOSS_ON_EXCHANGE.value)
and trade.close_profit and trade.close_profit < self._profit_limit)]
and trade.close_profit and trade.close_profit < 0)]
if self._only_per_side:
# Long or short trades only

View File

@@ -18,8 +18,7 @@ class ExchangeResolver(IResolver):
object_type = Exchange
@staticmethod
def load_exchange(exchange_name: str, config: dict, validate: bool = True,
load_leverage_tiers: bool = False) -> Exchange:
def load_exchange(exchange_name: str, config: dict, validate: bool = True) -> Exchange:
"""
Load the custom class from config parameter
:param exchange_name: name of the Exchange to load
@@ -30,13 +29,9 @@ class ExchangeResolver(IResolver):
exchange_name = exchange_name.title()
exchange = None
try:
exchange = ExchangeResolver._load_exchange(
exchange_name,
kwargs={
'config': config,
'validate': validate,
'load_leverage_tiers': load_leverage_tiers}
)
exchange = ExchangeResolver._load_exchange(exchange_name,
kwargs={'config': config,
'validate': validate})
except ImportError:
logger.info(
f"No {exchange_name} specific subclass found. Using the generic class instead.")

View File

@@ -282,7 +282,7 @@ def get_strategy(strategy: str, config=Depends(get_config)):
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
candletype: Optional[CandleType] = None, config=Depends(get_config)):
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv'))
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', None))
trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
pair_interval = dh.ohlcv_get_available_data(config['datadir'], trading_mode)

View File

@@ -37,7 +37,7 @@ def get_exchange(config=Depends(get_config)):
if not ApiServer._exchange:
from freqtrade.resolvers import ExchangeResolver
ApiServer._exchange = ExchangeResolver.load_exchange(
config['exchange']['name'], config, load_leverage_tiers=False)
config['exchange']['name'], config)
return ApiServer._exchange

View File

@@ -97,7 +97,7 @@ class RPC:
"""
self._freqtrade = freqtrade
self._config: Dict[str, Any] = freqtrade.config
if self._config.get('fiat_display_currency'):
if self._config.get('fiat_display_currency', None):
self._fiat_converter = CryptoToFiatConverter()
@staticmethod
@@ -566,7 +566,7 @@ class RPC:
else:
try:
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
rate = tickers.get(pair, {}).get('last')
rate = tickers.get(pair, {}).get('last', None)
if rate:
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
rate = 1.0 / rate

View File

@@ -243,22 +243,6 @@ class Telegram(RPCHandler):
"""
return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}"
def _add_analyzed_candle(self, pair: str) -> str:
candle_val = self._config['telegram'].get(
'notification_settings', {}).get('show_candle', 'off')
if candle_val != 'off':
if candle_val == 'ohlc':
analyzed_df, _ = self._rpc._freqtrade.dataprovider.get_analyzed_dataframe(
pair, self._config['timeframe'])
candle = analyzed_df.iloc[-1].squeeze() if len(analyzed_df) > 0 else None
if candle is not None:
return (
f"*Candle OHLC*: `{candle['open']}, {candle['high']}, "
f"{candle['low']}, {candle['close']}`\n"
)
return ''
def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
@@ -275,8 +259,7 @@ class Telegram(RPCHandler):
f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
f" (#{msg['trade_id']})\n"
)
message += self._add_analyzed_candle(msg['pair'])
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else ""
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else ""
message += f"*Amount:* `{msg['amount']:.8f}`\n"
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
message += f"*Leverage:* `{msg['leverage']}`\n"
@@ -289,7 +272,7 @@ class Telegram(RPCHandler):
message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
if msg.get('fiat_currency'):
if msg.get('fiat_currency', None):
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
message += ")`"
@@ -305,7 +288,7 @@ class Telegram(RPCHandler):
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
msg['emoji'] = self._get_sell_emoji(msg)
msg['leverage_text'] = (f"*Leverage:* `{msg['leverage']:.1f}`\n"
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0
if msg.get('leverage', None) and msg.get('leverage', 1.0) != 1.0
else "")
# Check if all sell properties are available.
@@ -323,7 +306,6 @@ class Telegram(RPCHandler):
message = (
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
f"{self._add_analyzed_candle(msg['pair'])}"
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
f"*Enter Tag:* `{msg['enter_tag']}`\n"

View File

@@ -45,21 +45,21 @@ class Webhook(RPCHandler):
try:
whconfig = self._config['webhook']
if msg['type'] in [RPCMessageType.ENTRY]:
valuedict = whconfig.get('webhookentry')
valuedict = whconfig.get('webhookentry', None)
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
valuedict = whconfig.get('webhookentrycancel')
valuedict = whconfig.get('webhookentrycancel', None)
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
valuedict = whconfig.get('webhookentryfill')
valuedict = whconfig.get('webhookentryfill', None)
elif msg['type'] == RPCMessageType.EXIT:
valuedict = whconfig.get('webhookexit')
valuedict = whconfig.get('webhookexit', None)
elif msg['type'] == RPCMessageType.EXIT_FILL:
valuedict = whconfig.get('webhookexitfill')
valuedict = whconfig.get('webhookexitfill', None)
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
valuedict = whconfig.get('webhookexitcancel')
valuedict = whconfig.get('webhookexitcancel', None)
elif msg['type'] in (RPCMessageType.STATUS,
RPCMessageType.STARTUP,
RPCMessageType.WARNING):
valuedict = whconfig.get('webhookstatus')
valuedict = whconfig.get('webhookstatus', None)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
if not valuedict:

View File

@@ -191,7 +191,6 @@ def detect_parameters(
and attr.category is not None and attr.category != category):
raise OperationalException(
f'Inconclusive parameter name {attr_name}, category: {attr.category}.')
if (category == attr.category or
(attr_name.startswith(category + '_') and attr.category is None)):
yield attr_name, attr

View File

@@ -442,8 +442,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: Optional[float], max_stake: float,
leverage: float, entry_tag: Optional[str], side: str,
**kwargs) -> float:
entry_tag: Optional[str], side: str, **kwargs) -> float:
"""
Customize stake size for each new trade.
@@ -453,7 +452,6 @@ class IStrategy(ABC, HyperStrategyMixin):
:param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:param leverage: Leverage selected for this trade.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A stake size, which is between min_stake and max_stake.

View File

@@ -51,13 +51,11 @@
"source": [
"# Load data using values set above\n",
"from freqtrade.data.history import load_pair_history\n",
"from freqtrade.enums import CandleType\n",
"\n",
"candles = load_pair_history(datadir=data_location,\n",
" timeframe=config[\"timeframe\"],\n",
" pair=pair,\n",
" data_format = \"hdf5\",\n",
" candle_type=CandleType.SPOT,\n",
" )\n",
"\n",
"# Confirm success\n",

View File

@@ -79,10 +79,9 @@ def custom_exit_price(self, pair: str, trade: 'Trade',
"""
return proposed_rate
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
proposed_stake: float, min_stake: Optional[float], max_stake: float,
leverage: float, entry_tag: Optional[str], side: str,
**kwargs) -> float:
entry_tag: 'Optional[str]', side: str, **kwargs) -> float:
"""
Customize stake size for each new trade.
@@ -92,7 +91,6 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f
:param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:param leverage: Leverage selected for this trade.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A stake size, which is between min_stake and max_stake.

View File

@@ -131,9 +131,9 @@ class Wallets:
if isinstance(balances[currency], dict):
self._wallets[currency] = Wallet(
currency,
balances[currency].get('free'),
balances[currency].get('used'),
balances[currency].get('total')
balances[currency].get('free', None),
balances[currency].get('used', None),
balances[currency].get('total', None)
)
# Remove currencies no longer in get_balances output
for currency in deepcopy(self._wallets):

View File

@@ -7,23 +7,23 @@
coveralls==3.3.1
flake8==4.0.1
flake8-tidy-imports==4.8.0
mypy==0.971
pre-commit==2.20.0
mypy==0.961
pre-commit==2.19.0
pytest==7.1.2
pytest-asyncio==0.19.0
pytest-asyncio==0.18.3
pytest-cov==3.0.0
pytest-mock==3.8.2
pytest-mock==3.7.0
pytest-random-order==1.0.4
isort==5.10.1
# For datetime mocking
time-machine==2.7.1
time-machine==2.7.0
# Convert jupyter notebooks to markdown documents
nbconvert==6.5.0
# mypy types
types-cachetools==5.2.1
types-cachetools==5.0.2
types-filelock==3.2.7
types-requests==2.28.3
types-tabulate==0.8.11
types-python-dateutil==2.8.19
types-requests==2.27.30
types-tabulate==0.8.9
types-python-dateutil==2.8.17

View File

@@ -1,4 +1,4 @@
# Include all requirements to run the bot.
-r requirements.txt
plotly==5.9.0
plotly==5.8.2

View File

@@ -1,21 +1,21 @@
numpy==1.23.1
pandas==1.4.3
numpy==1.23.0
pandas==1.4.2
pandas-ta==0.3.14b
ccxt==1.91.29
ccxt==1.88.15
# Pin cryptography for now due to rust build errors with piwheels
cryptography==37.0.4
cryptography==37.0.2
aiohttp==3.8.1
SQLAlchemy==1.4.39
python-telegram-bot==13.13
SQLAlchemy==1.4.37
python-telegram-bot==13.12
arrow==1.2.2
cachetools==4.2.2
requests==2.28.1
urllib3==1.26.10
jsonschema==4.7.2
requests==2.28.0
urllib3==1.26.9
jsonschema==4.6.0
TA-Lib==0.4.24
technical==1.3.0
tabulate==0.8.10
tabulate==0.8.9
pycoingecko==2.2.0
jinja2==3.1.2
tables==3.7.0
@@ -26,16 +26,16 @@ joblib==1.1.0
py_find_1st==1.1.5
# Load ticker files 30% faster
python-rapidjson==1.8
python-rapidjson==1.6
# Properly format api responses
orjson==3.7.8
orjson==3.7.2
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.79.0
uvicorn==0.18.2
fastapi==0.78.0
uvicorn==0.17.6
pyjwt==2.4.0
aiofiles==0.8.0
psutil==5.9.1
@@ -44,7 +44,7 @@ psutil==5.9.1
colorama==0.4.5
# Building config files interactively
questionary==1.10.0
prompt-toolkit==3.0.30
prompt-toolkit==3.0.29
# Extensions to datetime library
python-dateutil==2.8.2

View File

@@ -112,8 +112,11 @@ def patch_exchange(
mock_supported_modes=True
) -> None:
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
@@ -148,7 +151,7 @@ def get_patched_exchange(mocker, config, api_mock=None, id='binance',
patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes)
config['exchange']['name'] = id
try:
exchange = ExchangeResolver.load_exchange(id, config, load_leverage_tiers=True)
exchange = ExchangeResolver.load_exchange(id, config)
except ImportError:
exchange = Exchange(config)
return exchange
@@ -1691,7 +1694,6 @@ def limit_buy_order_old_partial():
'price': 0.00001099,
'amount': 90.99181073,
'filled': 23.0,
'cost': 90.99181073 * 23.0,
'remaining': 67.99181073,
'status': 'open'
}
@@ -2609,7 +2611,7 @@ def open_trade_usdt():
pair='ADA/USDT',
open_rate=2.0,
exchange='binance',
open_order_id='123456789_exit',
open_order_id='123456789',
amount=30.0,
fee_open=0.0,
fee_close=0.0,
@@ -2634,23 +2636,6 @@ def open_trade_usdt():
cost=trade.open_rate * trade.amount,
order_date=trade.open_date,
order_filled_date=trade.open_date,
),
Order(
ft_order_side='exit',
ft_pair=trade.pair,
ft_is_open=True,
order_id='123456789_exit',
status="open",
symbol=trade.pair,
order_type="limit",
side="sell",
price=trade.open_rate,
average=trade.open_rate,
filled=trade.amount,
remaining=0,
cost=trade.open_rate * trade.amount,
order_date=trade.open_date,
order_filled_date=trade.open_date,
)
]
return trade
@@ -3180,46 +3165,60 @@ def leverage_tiers():
"AAVE/USDT": [
{
'min': 0,
'max': 5000,
'max': 50000,
'mmr': 0.01,
'lev': 50,
'maintAmt': 0.0
},
{
'min': 5000,
'max': 25000,
'min': 50000,
'max': 250000,
'mmr': 0.02,
'lev': 25,
'maintAmt': 75.0
},
{
'min': 25000,
'max': 100000,
'mmr': 0.05,
'lev': 10,
'maintAmt': 700.0
},
{
'min': 100000,
'max': 250000,
'mmr': 0.1,
'lev': 5,
'maintAmt': 5700.0
'maintAmt': 500.0
},
{
'min': 250000,
'max': 1000000,
'mmr': 0.05,
'lev': 10,
'maintAmt': 8000.0
},
{
'min': 1000000,
'max': 2000000,
'mmr': 0.1,
'lev': 5,
'maintAmt': 58000.0
},
{
'min': 2000000,
'max': 5000000,
'mmr': 0.125,
'lev': 2,
'maintAmt': 11950.0
'lev': 4,
'maintAmt': 108000.0
},
{
'min': 5000000,
'max': 10000000,
'mmr': 0.1665,
'lev': 3,
'maintAmt': 315500.0
},
{
'min': 10000000,
'max': 50000000,
'mmr': 0.5,
'lev': 1,
'maintAmt': 386950.0
'max': 20000000,
'mmr': 0.25,
'lev': 2,
'maintAmt': 1150500.0
},
{
"min": 20000000,
"max": 50000000,
"mmr": 0.5,
"lev": 1,
"maintAmt": 6150500.0
}
],
"ADA/BUSD": [
{

View File

@@ -137,8 +137,7 @@ def exchange_futures(request, exchange_conf, class_mocker):
'freqtrade.exchange.binance.Binance.fill_leverage_tiers')
class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees')
class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init')
exchange = ExchangeResolver.load_exchange(
request.param, exchange_conf, validate=True, load_leverage_tiers=True)
exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True)
yield exchange, request.param
@@ -154,25 +153,6 @@ class TestCCXTExchange():
assert isinstance(markets[pair], dict)
assert exchange.market_is_spot(markets[pair])
def test_has_validations(self, exchange):
exchange, exchangename = exchange
exchange.validate_ordertypes({
'entry': 'limit',
'exit': 'limit',
'stoploss': 'limit',
})
if exchangename == 'gateio':
# gateio doesn't have market orders on spot
return
exchange.validate_ordertypes({
'entry': 'market',
'exit': 'market',
'stoploss': 'market',
})
def test_load_markets_futures(self, exchange_futures):
exchange, exchangename = exchange_futures
if not exchange:

View File

@@ -1135,58 +1135,7 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name, leverag
assert order["symbol"] == "ETH/BTC"
assert order["amount"] == 1
assert order["leverage"] == leverage
assert order["cost"] == 1 * 200
@pytest.mark.parametrize('side,is_short,order_reason', [
("buy", False, "entry"),
("sell", False, "exit"),
("buy", True, "exit"),
("sell", True, "entry"),
])
@pytest.mark.parametrize("order_type,price_side,fee", [
("limit", "same", 1.0),
("limit", "other", 2.0),
("market", "same", 2.0),
("market", "other", 2.0),
])
def test_create_dry_run_order_fees(
default_conf,
mocker,
side,
order_type,
is_short,
order_reason,
price_side,
fee,
):
mocker.patch(
'freqtrade.exchange.Exchange.get_fee',
side_effect=lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0
)
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled',
return_value=price_side == 'other')
exchange = get_patched_exchange(mocker, default_conf)
order = exchange.create_dry_run_order(
pair='LTC/USDT',
ordertype=order_type,
side=side,
amount=10,
rate=2.0,
leverage=1.0
)
if price_side == 'other' or order_type == 'market':
assert order['fee']['rate'] == fee
return
else:
assert order['fee'] is None
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled',
return_value=price_side != 'other')
order1 = exchange.fetch_dry_run_order(order['id'])
assert order1['fee']['rate'] == fee
assert order["cost"] == 1 * 200 / leverage
@pytest.mark.parametrize("side,startprice,endprice", [
@@ -2910,9 +2859,6 @@ def test_check_order_canceled_empty(mocker, default_conf, exchange_name, order,
({'amount': 10.0, 'fee': {}}, False),
({'result': 'testest123'}, False),
('hello_world', False),
({'status': 'canceled', 'amount': None, 'fee': None}, False),
({'status': 'canceled', 'filled': None, 'amount': None, 'fee': None}, False),
])
def test_is_cancel_order_result_suitable(mocker, default_conf, exchange_name, order, result):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
@@ -3598,7 +3544,7 @@ def test_order_has_fee(order, expected) -> None:
def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None:
mocker.patch('freqtrade.exchange.Exchange.calculate_fee_rate', MagicMock(return_value=0.01))
ex = get_patched_exchange(mocker, default_conf)
assert ex.extract_cost_curr_rate(order['fee'], order['symbol'], cost=20, amount=1) == expected
assert ex.extract_cost_curr_rate(order) == expected
@pytest.mark.parametrize("order,unknown_fee_rate,expected", [
@@ -3636,9 +3582,6 @@ def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None:
'fee': {'currency': 'POINT', 'cost': 2.0, 'rate': None}}, 1, 4.0),
({'symbol': 'POINT/BTC', 'amount': 0.04, 'cost': 0.5,
'fee': {'currency': 'POINT', 'cost': 2.0, 'rate': None}}, 2, 8.0),
# Missing currency
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': None, 'cost': 0.005}}, None, None),
])
def test_calculate_fee_rate(mocker, default_conf, order, expected, unknown_fee_rate) -> None:
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081})
@@ -3647,8 +3590,7 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected, unknown_fee_r
ex = get_patched_exchange(mocker, default_conf)
assert ex.calculate_fee_rate(order['fee'], order['symbol'],
cost=order['cost'], amount=order['amount']) == expected
assert ex.calculate_fee_rate(order) == expected
@pytest.mark.parametrize('retrycount,max_retries,expected', [

View File

@@ -53,25 +53,6 @@ def test_fetch_stoploss_order_gateio(default_conf, mocker):
assert fetch_order_mock.call_args_list[0][1]['pair'] == 'ETH/BTC'
assert fetch_order_mock.call_args_list[0][1]['params'] == {'stop': True}
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
exchange.fetch_order = MagicMock(return_value={
'status': 'closed',
'id': '1234',
'stopPrice': 5.62,
'info': {
'trade_id': '222555'
}
})
exchange.fetch_stoploss_order('1234', 'ETH/BTC')
assert exchange.fetch_order.call_count == 2
assert exchange.fetch_order.call_args_list[0][1]['order_id'] == '1234'
assert exchange.fetch_order.call_args_list[1][1]['order_id'] == '222555'
def test_cancel_stoploss_order_gateio(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='gateio')

View File

@@ -18,11 +18,11 @@ def hyperopt_conf(default_conf):
'runmode': RunMode.HYPEROPT,
'strategy': 'HyperoptableStrategy',
'hyperopt_loss': 'ShortTradeDurHyperOptLoss',
'hyperopt_path': str(Path(__file__).parent / 'hyperopts'),
'epochs': 1,
'timerange': None,
'spaces': ['default'],
'hyperopt_jobs': 1,
'hyperopt_path': str(Path(__file__).parent / 'hyperopts'),
'epochs': 1,
'timerange': None,
'spaces': ['default'],
'hyperopt_jobs': 1,
'hyperopt_min_trades': 1,
})
return hyperconf

View File

@@ -90,6 +90,28 @@ def load_data_test(what, testdatadir):
fill_missing=True)}
def simple_backtest(config, contour, mocker, testdatadir) -> None:
patch_exchange(mocker)
config['timeframe'] = '1m'
backtesting = Backtesting(config)
backtesting._set_strategy(backtesting.strategylist[0])
data = load_data_test(contour, testdatadir)
processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed)
assert isinstance(processed, dict)
results = backtesting.backtest(
processed=processed,
start_date=min_date,
end_date=max_date,
max_open_trades=1,
position_stacking=False,
enable_protections=config.get('enable_protections', False),
)
# results :: <class 'pandas.core.frame.DataFrame'>
return results
# FIX: fixturize this?
def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
data = history.load_data(datadir=datadir, timeframe='1m', pairs=[pair])
@@ -920,7 +942,6 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi
def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None:
# While this test IS a copy of test_backtest_pricecontours, it's needed to ensure
# results do not carry-over to the next run, which is not given by using parametrize.
patch_exchange(mocker)
default_conf['protections'] = [
{
"method": "CooldownPeriod",
@@ -928,7 +949,6 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
}]
default_conf['enable_protections'] = True
default_conf['timeframe'] = '1m'
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_max_pair_stake_amount", return_value=float('inf'))
@@ -939,27 +959,12 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
['sine', 9],
['raise', 10],
]
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
# While entry-signals are unrealistic, running backtesting
# over and over again should not cause different results
for [contour, numres] in tests:
# Debug output for random test failure
print(f"{contour}, {numres}")
data = load_data_test(contour, testdatadir)
processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed)
assert isinstance(processed, dict)
results = backtesting.backtest(
processed=processed,
start_date=min_date,
end_date=max_date,
max_open_trades=1,
position_stacking=False,
enable_protections=default_conf.get('enable_protections', False),
)
assert len(results['results']) == numres
assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres
@pytest.mark.parametrize('protections,contour,expected', [
@@ -985,25 +990,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
# While entry-signals are unrealistic, running backtesting
# over and over again should not cause different results
patch_exchange(mocker)
default_conf['timeframe'] = '1m'
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
data = load_data_test(contour, testdatadir)
processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed)
assert isinstance(processed, dict)
results = backtesting.backtest(
processed=processed,
start_date=min_date,
end_date=max_date,
max_open_trades=1,
position_stacking=False,
enable_protections=default_conf.get('enable_protections', False),
)
assert len(results['results']) == expected
assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == expected
def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):

View File

@@ -1,7 +1,7 @@
# pragma pylint: disable=missing-docstring,W0212,C0103
from datetime import datetime, timedelta
from pathlib import Path
from unittest.mock import ANY, MagicMock, PropertyMock
from unittest.mock import ANY, MagicMock
import pandas as pd
import pytest
@@ -18,8 +18,8 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.optimize.optimize_reports import generate_strategy_stats
from freqtrade.optimize.space import SKDecimal
from freqtrade.strategy import IntParameter
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, get_markets, log_has, log_has_re,
patch_exchange, patched_configuration_load_config_file)
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file)
def generate_result_metrics():
@@ -855,13 +855,12 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
'strategy': 'HyperoptableStrategy',
'user_data_dir': Path(tmpdir),
'hyperopt_random_state': 42,
'spaces': ['all'],
'spaces': ['all']
})
hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
assert hyperopt.backtesting.strategy.bot_loop_started is True
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
@@ -883,45 +882,6 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
hyperopt.get_optimizer([], 2)
def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, fee) -> None:
mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch('freqtrade.exchange.Exchange._load_markets')
mocker.patch('freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=get_markets()))
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
# No hyperopt needed
hyperopt_conf.update({
'strategy': 'HyperoptableStrategy',
'user_data_dir': Path(tmpdir),
'hyperopt_random_state': 42,
'spaces': ['all'],
# Enforce parallelity
'epochs': 2,
'hyperopt_jobs': 2,
'fee': fee.return_value,
})
hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.exchange.get_max_leverage = lambda *x, **xx: 1.0
hyperopt.backtesting.exchange.get_min_pair_stake_amount = lambda *x, **xx: 1.0
hyperopt.backtesting.exchange.get_max_pair_stake_amount = lambda *x, **xx: 100.0
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
assert hyperopt.backtesting.strategy.bot_loop_started is True
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
assert hyperopt.backtesting.strategy.sell_rsi.value == 74
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30
buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
assert isinstance(buy_rsi_range, range)
# Range from 0 - 50 (inclusive)
assert len(list(buy_rsi_range)) == 51
hyperopt.start()
def test_SKDecimal():
space = SKDecimal(1, 2, decimals=2)
assert 1.5 in space

View File

@@ -6,7 +6,6 @@ import pytest
from freqtrade import constants
from freqtrade.enums import ExitType
from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.trade_model import Order
from freqtrade.plugins.protectionmanager import ProtectionManager
from tests.conftest import get_patched_freqtradebot, log_has_re
@@ -31,37 +30,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool,
amount=0.01 / open_rate,
exchange='binance',
is_short=is_short,
leverage=1,
)
trade.orders.append(Order(
ft_order_side=trade.entry_side,
order_id=f'{pair}-{trade.entry_side}-{trade.open_date}',
ft_pair=pair,
amount=trade.amount,
filled=trade.amount,
remaining=0,
price=open_rate,
average=open_rate,
status="closed",
order_type="market",
side=trade.entry_side,
))
if not is_open:
trade.orders.append(Order(
ft_order_side=trade.exit_side,
order_id=f'{pair}-{trade.exit_side}-{trade.close_date}',
ft_pair=pair,
amount=trade.amount,
filled=trade.amount,
remaining=0,
price=open_rate * (2 - profit_rate if is_short else profit_rate),
average=open_rate * (2 - profit_rate if is_short else profit_rate),
status="closed",
order_type="market",
side=trade.exit_side,
))
trade.recalc_open_trade_value()
if not is_open:
trade.close(open_rate * (2 - profit_rate if is_short else profit_rate))
@@ -424,7 +393,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
@pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [
({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2, "stop_duration": 60},
"[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, "
"2 stoplosses with profit < 0.00% within 60 minutes.'}]",
"2 stoplosses within 60 minutes.'}]",
None
),
({"method": "CooldownPeriod", "stop_duration": 60},
@@ -442,9 +411,9 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
None
),
({"method": "StoplossGuard", "lookback_period_candles": 12, "trade_limit": 2,
"required_profit": -0.05, "stop_duration": 60},
"stop_duration": 60},
"[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, "
"2 stoplosses with profit < -5.00% within 12 candles.'}]",
"2 stoplosses within 12 candles.'}]",
None
),
({"method": "CooldownPeriod", "stop_duration_candles": 5},

View File

@@ -830,8 +830,6 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
assert cancel_order_mock.call_count == 2
assert trade.amount == amount
trade = Trade.query.filter(Trade.id == '3').first()
# make an limit-sell open trade
mocker.patch(
'freqtrade.exchange.Exchange.fetch_order',

View File

@@ -1398,10 +1398,10 @@ def test_api_strategies(botclient):
assert rc.json() == {'strategies': [
'HyperoptableStrategy',
'HyperoptableStrategyV2',
'InformativeDecoratorTest',
'StrategyTestV2',
'StrategyTestV3',
'StrategyTestV3Analysis',
'StrategyTestV3Futures'
]}

View File

@@ -12,7 +12,6 @@ from unittest.mock import ANY, MagicMock
import arrow
import pytest
from pandas import DataFrame
from telegram import Chat, Message, ReplyKeyboardMarkup, Update
from telegram.error import BadRequest, NetworkError, TelegramError
@@ -686,7 +685,6 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f
# Simulate fulfilled LIMIT_SELL order for trade
oobj = Order.parse_from_ccxt_object(
limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell')
trade.orders.append(oobj)
trade.update_trade(oobj)
trade.close_date = datetime.now(timezone.utc)
@@ -708,7 +706,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f
assert '*Best Performing:* `ETH/USDT: 9.45%`' in msg_mock.call_args_list[-1][0][0]
assert '*Max Drawdown:*' in msg_mock.call_args_list[-1][0][0]
assert '*Profit factor:*' in msg_mock.call_args_list[-1][0][0]
assert '*Trading volume:* `126 USDT`' in msg_mock.call_args_list[-1][0][0]
assert '*Trading volume:* `60 USDT`' in msg_mock.call_args_list[-1][0][0]
@pytest.mark.parametrize('is_short', [True, False])
@@ -1657,17 +1655,8 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
(RPCMessageType.ENTRY, 'Long', 'long_signal_01', 1.0),
(RPCMessageType.ENTRY, 'Long', 'long_signal_01', 5.0),
(RPCMessageType.ENTRY, 'Short', 'short_signal_01', 2.0)])
def test_send_msg_enter_notification(default_conf, mocker, caplog, message_type,
enter, enter_signal, leverage) -> None:
default_conf['telegram']['notification_settings']['show_candle'] = 'ohlc'
df = DataFrame({
'open': [1.1],
'high': [2.2],
'low': [1.0],
'close': [1.5],
})
mocker.patch('freqtrade.data.dataprovider.DataProvider.get_analyzed_dataframe',
return_value=(df, 1))
def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
enter, enter_signal, leverage) -> None:
msg = {
'type': message_type,
@@ -1685,7 +1674,6 @@ def test_send_msg_enter_notification(default_conf, mocker, caplog, message_type,
'fiat_currency': 'USD',
'current_rate': 1.099e-05,
'amount': 1333.3333333333335,
'analyzed_candle': {'open': 1.1, 'high': 2.2, 'low': 1.0, 'close': 1.5},
'open_date': arrow.utcnow().shift(hours=-1)
}
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
@@ -1695,7 +1683,6 @@ def test_send_msg_enter_notification(default_conf, mocker, caplog, message_type,
assert msg_mock.call_args[0][0] == (
f'\N{LARGE BLUE CIRCLE} *Binance (dry):* {enter} ETH/BTC (#1)\n'
'*Candle OHLC*: `1.1, 2.2, 1.0, 1.5`\n'
f'*Enter Tag:* `{enter_signal}`\n'
'*Amount:* `1333.33333333`\n'
f'{leverage_text}'
@@ -1723,8 +1710,7 @@ def test_send_msg_enter_notification(default_conf, mocker, caplog, message_type,
@pytest.mark.parametrize('message_type,enter_signal', [
(RPCMessageType.ENTRY_CANCEL, 'long_signal_01'),
(RPCMessageType.ENTRY_CANCEL, 'short_signal_01')])
def test_send_msg_enter_cancel_notification(
default_conf, mocker, message_type, enter_signal) -> None:
def test_send_msg_buy_cancel_notification(default_conf, mocker, message_type, enter_signal) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)

View File

@@ -1,13 +1,13 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
from pandas import DataFrame
from strategy_test_v3 import StrategyTestV3
from strategy_test_v2 import StrategyTestV2
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter
class HyperoptableStrategy(StrategyTestV3):
class HyperoptableStrategy(StrategyTestV2):
"""
Default Strategy provided by freqtrade bot.
Please do not modify this strategy, it's intended for internal use only.
@@ -44,11 +44,6 @@ class HyperoptableStrategy(StrategyTestV3):
})
return prot
bot_loop_started = False
def bot_loop_start(self):
self.bot_loop_started = True
def bot_start(self, **kwargs) -> None:
"""
Parameters can also be defined here ...

View File

@@ -1,54 +0,0 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
from strategy_test_v2 import StrategyTestV2
from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter
class HyperoptableStrategyV2(StrategyTestV2):
"""
Default Strategy provided by freqtrade bot.
Please do not modify this strategy, it's intended for internal use only.
Please look at the SampleStrategy in the user_data/strategy directory
or strategy repository https://github.com/freqtrade/freqtrade-strategies
for samples and inspiration.
"""
buy_params = {
'buy_rsi': 35,
# Intentionally not specified, so "default" is tested
# 'buy_plusdi': 0.4
}
sell_params = {
'sell_rsi': 74,
'sell_minusdi': 0.4
}
buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
load=False)
protection_enabled = BooleanParameter(default=True)
protection_cooldown_lookback = IntParameter([0, 50], default=30)
@property
def protections(self):
prot = []
if self.protection_enabled.value:
prot.append({
"method": "CooldownPeriod",
"stop_duration_candles": self.protection_cooldown_lookback.value
})
return prot
bot_loop_started = False
def bot_loop_start(self):
self.bot_loop_started = True
def bot_start(self, **kwargs) -> None:
"""
Parameters can also be defined here ...
"""
self.buy_rsi = IntParameter([0, 50], default=30, space='buy')

View File

@@ -0,0 +1,175 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
import talib.abstract as ta
from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy,
RealParameter)
class StrategyTestV3Analysis(IStrategy):
"""
Strategy used by tests freqtrade bot.
Please do not modify this strategy, it's intended for internal use only.
Please look at the SampleStrategy in the user_data/strategy directory
or strategy repository https://github.com/freqtrade/freqtrade-strategies
for samples and inspiration.
"""
INTERFACE_VERSION = 3
# Minimal ROI designed for the strategy
minimal_roi = {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
}
# Optimal stoploss designed for the strategy
stoploss = -0.10
# Optimal timeframe for the strategy
timeframe = '5m'
# Optional order type mapping
order_types = {
'entry': 'limit',
'exit': 'limit',
'stoploss': 'limit',
'stoploss_on_exchange': False
}
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 20
# Optional time in force for orders
order_time_in_force = {
'entry': 'gtc',
'exit': 'gtc',
}
buy_params = {
'buy_rsi': 35,
# Intentionally not specified, so "default" is tested
# 'buy_plusdi': 0.4
}
sell_params = {
'sell_rsi': 74,
'sell_minusdi': 0.4
}
buy_rsi = IntParameter([0, 50], default=30, space='buy')
buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
load=False)
protection_enabled = BooleanParameter(default=True)
protection_cooldown_lookback = IntParameter([0, 50], default=30)
# TODO: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... )
# @property
# def protections(self):
# prot = []
# if self.protection_enabled.value:
# prot.append({
# "method": "CooldownPeriod",
# "stop_duration_candles": self.protection_cooldown_lookback.value
# })
# return prot
bot_started = False
def bot_start(self):
self.bot_started = True
def informative_pairs(self):
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Momentum Indicator
# ------------------------------------
# ADX
dataframe['adx'] = ta.ADX(dataframe)
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# Minus Directional Indicator / Movement
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Stoch fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
# EMA - Exponential Moving Average
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe['rsi'] < self.buy_rsi.value) &
(dataframe['fastd'] < 35) &
(dataframe['adx'] > 30) &
(dataframe['plus_di'] > self.buy_plusdi.value)
) |
(
(dataframe['adx'] > 65) &
(dataframe['plus_di'] > self.buy_plusdi.value)
),
['enter_long', 'enter_tag']] = 1, 'enter_tag_long'
dataframe.loc[
(
qtpylib.crossed_below(dataframe['rsi'], self.sell_rsi.value)
),
['enter_short', 'enter_tag']] = 1, 'enter_tag_short'
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(
(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) |
(qtpylib.crossed_above(dataframe['fastd'], 70))
) &
(dataframe['adx'] > 10) &
(dataframe['minus_di'] > 0)
) |
(
(dataframe['adx'] > 70) &
(dataframe['minus_di'] > self.sell_minusdi.value)
),
['exit_long', 'exit_tag']] = 1, 'exit_tag_long'
dataframe.loc[
(
qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)
),
['exit_long', 'exit_tag']] = 1, 'exit_tag_short'
return dataframe

View File

@@ -916,7 +916,7 @@ def test_hyperopt_parameters():
def test_auto_hyperopt_interface(default_conf):
default_conf.update({'strategy': 'HyperoptableStrategyV2'})
default_conf.update({'strategy': 'HyperoptableStrategy'})
PairLocks.timeframe = default_conf['timeframe']
strategy = StrategyResolver.load_strategy(default_conf)
strategy.ft_bot_start()

View File

@@ -2060,9 +2060,8 @@ def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) ->
@pytest.mark.parametrize("is_short", [False, True])
def test_update_trade_state_sell(
default_conf_usdt, trades_for_order, limit_order_open, limit_order, is_short, mocker
default_conf_usdt, trades_for_order, limit_order_open, limit_order, is_short, mocker,
):
buy_order = limit_order[entry_side(is_short)]
open_order = limit_order_open[exit_side(is_short)]
l_order = limit_order[exit_side(is_short)]
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
@@ -2089,9 +2088,6 @@ def test_update_trade_state_sell(
leverage=1,
is_short=is_short,
)
order = Order.parse_from_ccxt_object(buy_order, 'LTC/ETH', entry_side(is_short))
trade.orders.append(order)
order = Order.parse_from_ccxt_object(open_order, 'LTC/ETH', exit_side(is_short))
trade.orders.append(order)
assert order.status == 'open'
@@ -2139,6 +2135,8 @@ def test_handle_trade(
assert trade
time.sleep(0.01) # Race condition fix
oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], entry_side(is_short))
trade.update_trade(oobj)
assert trade.is_open is True
freqtrade.wallets.update()
@@ -2148,15 +2146,11 @@ def test_handle_trade(
assert trade.open_order_id == exit_order['id']
# Simulate fulfilled LIMIT_SELL order for trade
trade.orders[-1].ft_is_open = False
trade.orders[-1].status = 'closed'
trade.orders[-1].filled = trade.orders[-1].remaining
trade.orders[-1].remaining = 0.0
oobj = Order.parse_from_ccxt_object(exit_order, exit_order['symbol'], exit_side(is_short))
trade.update_trade(oobj)
trade.update_trade(trade.orders[-1])
assert trade.close_rate == (2.0 if is_short else 2.2)
assert pytest.approx(trade.close_profit) == close_profit
assert trade.close_rate == 2.0 if is_short else 2.2
assert trade.close_profit == close_profit
assert trade.calc_profit(trade.close_rate) == 5.685
assert trade.close_date is not None
assert trade.exit_reason == 'sell_signal1'
@@ -2759,8 +2753,6 @@ def test_check_handle_cancelled_exit(
cancel_order_mock = MagicMock()
limit_sell_order_old.update({"status": "canceled", 'filled': 0.0})
limit_sell_order_old['side'] = 'buy' if is_short else 'sell'
limit_sell_order_old['id'] = open_trade_usdt.open_order_id
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -2795,7 +2787,6 @@ def test_manage_open_orders_partial(
rpc_mock = patch_RPCManager(mocker)
open_trade.is_short = is_short
open_trade.leverage = leverage
open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy'
limit_buy_order_old_partial['id'] = open_trade.open_order_id
limit_buy_order_old_partial['side'] = 'sell' if is_short else 'buy'
limit_buy_canceled = deepcopy(limit_buy_order_old_partial)
@@ -2881,7 +2872,6 @@ def test_manage_open_orders_partial_except(
limit_buy_order_old_partial_canceled, mocker
) -> None:
open_trade.is_short = is_short
open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy'
rpc_mock = patch_RPCManager(mocker)
limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id
limit_buy_order_old_partial['id'] = open_trade.open_order_id
@@ -3100,27 +3090,7 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
close_date=arrow.utcnow().datetime,
exit_reason="sell_reason_whatever",
)
trade.orders = [
Order(
ft_order_side='buy',
ft_pair=trade.pair,
ft_is_open=True,
order_id='123456',
status="closed",
symbol=trade.pair,
order_type="market",
side="buy",
price=trade.open_rate,
average=trade.open_rate,
filled=trade.amount,
remaining=0,
cost=trade.open_rate * trade.amount,
order_date=trade.open_date,
order_filled_date=trade.open_date,
),
]
order = {'id': "123456",
'remaining': 1,
order = {'remaining': 1,
'amount': 1,
'status': "open"}
reason = CANCEL_REASON['TIMEOUT']
@@ -3656,7 +3626,7 @@ def test_execute_trade_exit_market_order(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker_usdt,
get_fee=fee,
_is_dry_limit_order_filled=MagicMock(return_value=True),
_is_dry_limit_order_filled=MagicMock(return_value=False),
)
patch_whitelist(mocker, default_conf_usdt)
freqtrade = FreqtradeBot(default_conf_usdt)
@@ -3672,8 +3642,7 @@ def test_execute_trade_exit_market_order(
# Increase the price and sell it
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker_usdt_sell_up,
_is_dry_limit_order_filled=MagicMock(return_value=False),
fetch_ticker=ticker_usdt_sell_up
)
freqtrade.config['order_types']['exit'] = 'market'
@@ -3686,7 +3655,7 @@ def test_execute_trade_exit_market_order(
assert not trade.is_open
assert trade.close_profit == profit_ratio
assert rpc_mock.call_count == 4
assert rpc_mock.call_count == 3
last_msg = rpc_mock.call_args_list[-2][0][0]
assert {
'type': RPCMessageType.EXIT,
@@ -3982,9 +3951,9 @@ def test_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limit_order_
# Test if entry-signal is absent (should sell due to roi = true)
if is_short:
patch_get_signal(freqtrade, enter_long=False, exit_short=False, exit_tag='something')
patch_get_signal(freqtrade, enter_long=False, exit_short=False)
else:
patch_get_signal(freqtrade, enter_long=False, exit_long=False, exit_tag='something')
patch_get_signal(freqtrade, enter_long=False, exit_long=False)
assert freqtrade.handle_trade(trade) is True
assert trade.exit_reason == ExitType.ROI.value

View File

@@ -481,7 +481,6 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_
trade.open_order_id = 'something'
oobj = Order.parse_from_ccxt_object(enter_order, 'ADA/USDT', entry_side)
trade.orders.append(oobj)
trade.update_trade(oobj)
assert trade.open_order_id is None
assert trade.open_rate == open_rate
@@ -497,12 +496,11 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_
trade.open_order_id = 'something'
time_machine.move_to("2022-03-31 21:45:05 +00:00")
oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', exit_side)
trade.orders.append(oobj)
trade.update_trade(oobj)
assert trade.open_order_id is None
assert trade.close_rate == close_rate
assert pytest.approx(trade.close_profit) == profit
assert trade.close_profit == profit
assert trade.close_date is not None
assert log_has_re(f"LIMIT_{exit_side.upper()} has been fulfilled for "
r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, "
@@ -531,7 +529,6 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
trade.open_order_id = 'something'
oobj = Order.parse_from_ccxt_object(market_buy_order_usdt, 'ADA/USDT', 'buy')
trade.orders.append(oobj)
trade.update_trade(oobj)
assert trade.open_order_id is None
assert trade.open_rate == 2.0
@@ -546,11 +543,10 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
trade.is_open = True
trade.open_order_id = 'something'
oobj = Order.parse_from_ccxt_object(market_sell_order_usdt, 'ADA/USDT', 'sell')
trade.orders.append(oobj)
trade.update_trade(oobj)
assert trade.open_order_id is None
assert trade.close_rate == 2.2
assert pytest.approx(trade.close_profit) == 0.094513715710723
assert trade.close_profit == round(0.0945137157107232, 8)
assert trade.close_date is not None
assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, "
r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, "
@@ -628,41 +624,14 @@ def test_trade_close(limit_buy_order_usdt, limit_sell_order_usdt, fee):
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10),
interest_rate=0.0005,
exchange='binance',
trading_mode=margin,
leverage=1.0,
trading_mode=margin
)
trade.orders.append(Order(
ft_order_side=trade.entry_side,
order_id=f'{trade.pair}-{trade.entry_side}-{trade.open_date}',
ft_pair=trade.pair,
amount=trade.amount,
filled=trade.amount,
remaining=0,
price=trade.open_rate,
average=trade.open_rate,
status="closed",
order_type="limit",
side=trade.entry_side,
))
trade.orders.append(Order(
ft_order_side=trade.exit_side,
order_id=f'{trade.pair}-{trade.exit_side}-{trade.open_date}',
ft_pair=trade.pair,
amount=trade.amount,
filled=trade.amount,
remaining=0,
price=2.2,
average=2.2,
status="closed",
order_type="limit",
side=trade.exit_side,
))
assert trade.close_profit is None
assert trade.close_date is None
assert trade.is_open is True
trade.close(2.2)
assert trade.is_open is False
assert pytest.approx(trade.close_profit) == 0.094513715
assert trade.close_profit == round(0.0945137157107232, 8)
assert trade.close_date is not None
new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime,
@@ -1231,7 +1200,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000',
0.0, 0.0, 0.0, '5m',
'buy_order', 'dry_stop_order_id222')
'buy_order', 'stop_order_id222')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
@@ -1257,7 +1226,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
'buy',
'ETC/BTC',
0,
'dry_buy_order',
'buy_order',
'closed',
'ETC/BTC',
'limit',
@@ -1269,44 +1238,12 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
{amount * 0.00258580}
),
(
1,
'buy',
'ETC/BTC',
1,
'dry_buy_order22',
'canceled',
'ETC/BTC',
'limit',
'buy',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
),
(
1,
'stoploss',
'ETC/BTC',
1,
'dry_stop_order_id11X',
'canceled',
'ETC/BTC',
'limit',
'sell',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
),
(
1,
'stoploss',
'ETC/BTC',
1,
'dry_stop_order_id222',
'open',
'stop_order_id222',
'closed',
'ETC/BTC',
'limit',
'sell',
@@ -1355,7 +1292,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.exit_reason is None
assert trade.strategy is None
assert trade.timeframe == '5m'
assert trade.stoploss_order_id == 'dry_stop_order_id222'
assert trade.stoploss_order_id == 'stop_order_id222'
assert trade.stoploss_last_update is None
assert log_has("trying trades_bak1", caplog)
assert log_has("trying trades_bak2", caplog)
@@ -1365,21 +1302,12 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.close_profit_abs is None
orders = trade.orders
assert len(orders) == 4
assert orders[0].order_id == 'dry_buy_order'
assert len(orders) == 2
assert orders[0].order_id == 'buy_order'
assert orders[0].ft_order_side == 'buy'
assert orders[-1].order_id == 'dry_stop_order_id222'
assert orders[-1].ft_order_side == 'stoploss'
assert orders[-1].ft_is_open is True
assert orders[1].order_id == 'dry_buy_order22'
assert orders[1].ft_order_side == 'buy'
assert orders[1].ft_is_open is False
assert orders[2].order_id == 'dry_stop_order_id11X'
assert orders[2].ft_order_side == 'stoploss'
assert orders[2].ft_is_open is False
assert orders[1].order_id == 'stop_order_id222'
assert orders[1].ft_order_side == 'stoploss'
def test_migrate_too_old(mocker, default_conf, fee, caplog):

View File

@@ -72,7 +72,7 @@ def test_add_indicators(default_conf, testdatadir, caplog):
strategy = StrategyResolver.load_strategy(default_conf)
# Generate entry/exit signals and indicators
# Generate buy/sell signals and indicators
data = strategy.analyze_ticker(data, {'pair': pair})
fig = generate_empty_figure()
@@ -113,7 +113,7 @@ def test_add_areas(default_conf, testdatadir, caplog):
ind_plain = {"macd": {"fill_to": "macdhist"}}
strategy = StrategyResolver.load_strategy(default_conf)
# Generate entry/exit signals and indicators
# Generate buy/sell signals and indicators
data = strategy.analyze_ticker(data, {'pair': pair})
fig = generate_empty_figure()
@@ -165,24 +165,24 @@ def test_plot_trades(testdatadir, caplog):
fig = plot_trades(fig, trades)
figure = fig1.layout.figure
# Check entry - color, should be in first graph, ...
trade_entries = find_trace_in_fig_data(figure.data, 'Trade entry')
assert isinstance(trade_entries, go.Scatter)
assert trade_entries.yaxis == 'y'
assert len(trades) == len(trade_entries.x)
assert trade_entries.marker.color == 'cyan'
assert trade_entries.marker.symbol == 'circle-open'
assert trade_entries.text[0] == '3.99%, buy_tag, roi, 15 min'
# Check buys - color, should be in first graph, ...
trade_buy = find_trace_in_fig_data(figure.data, 'Trade buy')
assert isinstance(trade_buy, go.Scatter)
assert trade_buy.yaxis == 'y'
assert len(trades) == len(trade_buy.x)
assert trade_buy.marker.color == 'cyan'
assert trade_buy.marker.symbol == 'circle-open'
assert trade_buy.text[0] == '3.99%, buy_tag, roi, 15 min'
trade_exit = find_trace_in_fig_data(figure.data, 'Exit - Profit')
assert isinstance(trade_exit, go.Scatter)
assert trade_exit.yaxis == 'y'
assert len(trades.loc[trades['profit_ratio'] > 0]) == len(trade_exit.x)
assert trade_exit.marker.color == 'green'
assert trade_exit.marker.symbol == 'square-open'
assert trade_exit.text[0] == '3.99%, buy_tag, roi, 15 min'
trade_sell = find_trace_in_fig_data(figure.data, 'Sell - Profit')
assert isinstance(trade_sell, go.Scatter)
assert trade_sell.yaxis == 'y'
assert len(trades.loc[trades['profit_ratio'] > 0]) == len(trade_sell.x)
assert trade_sell.marker.color == 'green'
assert trade_sell.marker.symbol == 'square-open'
assert trade_sell.text[0] == '3.99%, buy_tag, roi, 15 min'
trade_sell_loss = find_trace_in_fig_data(figure.data, 'Exit - Loss')
trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss')
assert isinstance(trade_sell_loss, go.Scatter)
assert trade_sell_loss.yaxis == 'y'
assert len(trades.loc[trades['profit_ratio'] <= 0]) == len(trade_sell_loss.x)