Merge branch 'develop' into pr/GluTbl/5756
This commit is contained in:
commit
d1209fe415
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -9,7 +9,7 @@ assignees: ''
|
|||||||
<!--
|
<!--
|
||||||
Have you searched for similar issues before posting it?
|
Have you searched for similar issues before posting it?
|
||||||
|
|
||||||
If you have discovered a bug in the bot, please [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
|
If you have discovered a bug in the bot, please [search the issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
|
||||||
If it hasn't been reported, please create a new issue.
|
If it hasn't been reported, please create a new issue.
|
||||||
|
|
||||||
Please do not use bug reports to request new features.
|
Please do not use bug reports to request new features.
|
||||||
|
2
.github/ISSUE_TEMPLATE/question.md
vendored
2
.github/ISSUE_TEMPLATE/question.md
vendored
@ -22,4 +22,4 @@ Please do not use the question template to report bugs or to request new feature
|
|||||||
|
|
||||||
## Your question
|
## Your question
|
||||||
|
|
||||||
*Ask the question you have not been able to find an answer in our [Documentation](https://www.freqtrade.io/en/latest/)*
|
*Ask the question you have not been able to find an answer in the [Documentation](https://www.freqtrade.io/en/latest/)*
|
||||||
|
@ -56,6 +56,13 @@ To help with that, we encourage you to install the git pre-commit
|
|||||||
hook that will warn you when you try to commit code that fails these checks.
|
hook that will warn you when you try to commit code that fails these checks.
|
||||||
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
|
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
|
||||||
|
|
||||||
|
##### Additional styles applied
|
||||||
|
|
||||||
|
* Have docstrings on all public methods
|
||||||
|
* Use double-quotes for docstrings
|
||||||
|
* Multiline docstrings should be indented to the level of the first quote
|
||||||
|
* Doc-strings should follow the reST format (`:param xxx: ...`, `:return: ...`, `:raises KeyError: ... `)
|
||||||
|
|
||||||
### 3. Test if all type-hints are correct
|
### 3. Test if all type-hints are correct
|
||||||
|
|
||||||
#### Run mypy
|
#### Run mypy
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.9.7-slim-buster as base
|
FROM python:3.9.9-slim-bullseye as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
17
README.md
17
README.md
@ -28,9 +28,10 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
|
|||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
|
- [X] [Kraken](https://kraken.com/)
|
||||||
|
- [X] [OKEX](https://www.okex.com/)
|
||||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
@ -44,7 +45,7 @@ Exchanges confirmed working by the community:
|
|||||||
|
|
||||||
We invite you to read the bot documentation to ensure you understand how the bot is working.
|
We invite you to read the bot documentation to ensure you understand how the bot is working.
|
||||||
|
|
||||||
Please find the complete documentation on our [website](https://www.freqtrade.io).
|
Please find the complete documentation on the [freqtrade website](https://www.freqtrade.io).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -121,7 +122,7 @@ optional arguments:
|
|||||||
|
|
||||||
### Telegram RPC commands
|
### Telegram RPC commands
|
||||||
|
|
||||||
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
|
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on the [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
|
||||||
|
|
||||||
- `/start`: Starts the trader.
|
- `/start`: Starts the trader.
|
||||||
- `/stop`: Stops the trader.
|
- `/stop`: Stops the trader.
|
||||||
@ -152,10 +153,10 @@ For any questions not covered by the documentation or for further information ab
|
|||||||
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||||
|
|
||||||
If you discover a bug in the bot, please
|
If you discover a bug in the bot, please
|
||||||
[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
[search the issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||||
first. If it hasn't been reported, please
|
first. If it hasn't been reported, please
|
||||||
[create a new issue](https://github.com/freqtrade/freqtrade/issues/new/choose) and
|
[create a new issue](https://github.com/freqtrade/freqtrade/issues/new/choose) and
|
||||||
ensure you follow the template guide so that our team can assist you as
|
ensure you follow the template guide so that the team can assist you as
|
||||||
quickly as possible.
|
quickly as possible.
|
||||||
|
|
||||||
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
||||||
@ -169,13 +170,13 @@ in the bug reports.
|
|||||||
|
|
||||||
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
|
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
|
||||||
|
|
||||||
Feel like our bot is missing a feature? We welcome your pull requests!
|
Feel like the bot is missing a feature? We welcome your pull requests!
|
||||||
|
|
||||||
Please read our
|
Please read the
|
||||||
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
to understand the requirements before sending your pull-requests.
|
to understand the requirements before sending your pull-requests.
|
||||||
|
|
||||||
Coding is not a necessity to contribute - maybe start with improving our documentation?
|
Coding is not a necessity to contribute - maybe start with improving the documentation?
|
||||||
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
|
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
|
||||||
|
|
||||||
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/p7nuUNVfP7) (please use the #dev channel for this). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/p7nuUNVfP7) (please use the #dev channel for this). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"buy": 10,
|
||||||
"sell": 30,
|
"sell": 30,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
"unit": "minutes"
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.7.10-slim-buster as base
|
FROM python:3.9.9-slim-bullseye as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
BIN
docs/assets/frequi_url.png
Normal file
BIN
docs/assets/frequi_url.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -21,6 +21,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[--timeframe-detail TIMEFRAME_DETAIL]
|
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
[--export {none,trades}] [--export-filename PATH]
|
[--export {none,trades}] [--export-filename PATH]
|
||||||
|
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
@ -30,7 +31,7 @@ optional arguments:
|
|||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
--data-format-ohlcv {json,jsongz,hdf5}
|
--data-format-ohlcv {json,jsongz,hdf5}
|
||||||
Storage format for downloaded candle (OHLCV) data.
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
(default: `None`).
|
(default: `json`).
|
||||||
--max-open-trades INT
|
--max-open-trades INT
|
||||||
Override the value of the `max_open_trades`
|
Override the value of the `max_open_trades`
|
||||||
configuration setting.
|
configuration setting.
|
||||||
@ -65,8 +66,7 @@ optional arguments:
|
|||||||
set either in config or via command line. When using
|
set either in config or via command line. When using
|
||||||
this together with `--export trades`, the strategy-
|
this together with `--export trades`, the strategy-
|
||||||
name is injected into the filename (so `backtest-
|
name is injected into the filename (so `backtest-
|
||||||
data.json` becomes `backtest-data-
|
data.json` becomes `backtest-data-SampleStrategy.json`
|
||||||
SampleStrategy.json`
|
|
||||||
--export {none,trades}
|
--export {none,trades}
|
||||||
Export backtest results (default: trades).
|
Export backtest results (default: trades).
|
||||||
--export-filename PATH
|
--export-filename PATH
|
||||||
@ -74,6 +74,8 @@ optional arguments:
|
|||||||
Requires `--export` to be set as well. Example:
|
Requires `--export` to be set as well. Example:
|
||||||
`--export-filename=user_data/backtest_results/backtest
|
`--export-filename=user_data/backtest_results/backtest
|
||||||
_today.json`
|
_today.json`
|
||||||
|
--breakdown {day,week,month} [{day,week,month} ...]
|
||||||
|
Show backtesting breakdown per [day, week, month].
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
@ -113,7 +115,7 @@ The result of backtesting will confirm if your bot has better odds of making a p
|
|||||||
All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation.
|
All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation.
|
||||||
|
|
||||||
!!! Warning "Using dynamic pairlists for backtesting"
|
!!! Warning "Using dynamic pairlists for backtesting"
|
||||||
Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
|
Using dynamic pairlists is possible (not all of the handlers are allowed to be used in backtest mode), however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
|
||||||
Also, when using pairlists other than StaticPairlist, reproducibility of backtesting-results cannot be guaranteed.
|
Also, when using pairlists other than StaticPairlist, reproducibility of backtesting-results cannot be guaranteed.
|
||||||
Please read the [pairlists documentation](plugins.md#pairlists) for more information.
|
Please read the [pairlists documentation](plugins.md#pairlists) for more information.
|
||||||
|
|
||||||
@ -429,10 +431,35 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
||||||
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
|
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
|
||||||
|
|
||||||
|
### Daily / Weekly / Monthly breakdown
|
||||||
|
|
||||||
|
You can get an overview over daily / weekly or monthly results by using the `--breakdown <>` switch.
|
||||||
|
|
||||||
|
To visualize daily and weekly breakdowns, you can use the following:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade backtesting --strategy MyAwesomeStrategy --breakdown day month
|
||||||
|
```
|
||||||
|
|
||||||
|
``` output
|
||||||
|
======================== DAY BREAKDOWN =========================
|
||||||
|
| Day | Tot Profit USDT | Wins | Draws | Losses |
|
||||||
|
|------------+-------------------+--------+---------+----------|
|
||||||
|
| 03/07/2021 | 200.0 | 2 | 0 | 0 |
|
||||||
|
| 04/07/2021 | -50.31 | 0 | 0 | 2 |
|
||||||
|
| 05/07/2021 | 220.611 | 3 | 2 | 0 |
|
||||||
|
| 06/07/2021 | 150.974 | 3 | 0 | 2 |
|
||||||
|
| 07/07/2021 | -70.193 | 1 | 0 | 2 |
|
||||||
|
| 08/07/2021 | 212.413 | 2 | 0 | 3 |
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The output will show a table containing the realized absolute Profit (in stake currency) for the given timeperiod, as well as wins, draws and losses that materialized (closed) on this day.
|
||||||
|
|
||||||
### Further backtest-result analysis
|
### Further backtest-result analysis
|
||||||
|
|
||||||
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
||||||
You can then load the trades to perform further analysis as shown in our [data analysis](data-analysis.md#backtesting) backtesting section.
|
You can then load the trades to perform further analysis as shown in the [data analysis](data-analysis.md#backtesting) backtesting section.
|
||||||
|
|
||||||
## Assumptions made by backtesting
|
## Assumptions made by backtesting
|
||||||
|
|
||||||
@ -451,6 +478,7 @@ Since backtesting lacks some detailed information about what happens within a ca
|
|||||||
- Low happens before high for stoploss, protecting capital first
|
- Low happens before high for stoploss, protecting capital first
|
||||||
- Trailing stoploss
|
- Trailing stoploss
|
||||||
- Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered)
|
- Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered)
|
||||||
|
- On trade entry candles that trigger trailing stoploss, the "minimum offset" (`stop_positive_offset`) is assumed (instead of high) - and the stop is calculated from this point
|
||||||
- High happens first - adjusting stoploss
|
- High happens first - adjusting stoploss
|
||||||
- Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly)
|
- Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly)
|
||||||
- ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies
|
- ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies
|
||||||
|
@ -37,6 +37,15 @@ Using this scheme, all configuration settings will also be available as environm
|
|||||||
|
|
||||||
Please note that Environment variables will overwrite corresponding settings in your configuration, but command line Arguments will always win.
|
Please note that Environment variables will overwrite corresponding settings in your configuration, but command line Arguments will always win.
|
||||||
|
|
||||||
|
Common example:
|
||||||
|
|
||||||
|
```
|
||||||
|
FREQTRADE__TELEGRAM__CHAT_ID=<telegramchatid>
|
||||||
|
FREQTRADE__TELEGRAM__TOKEN=<telegramToken>
|
||||||
|
FREQTRADE__EXCHANGE__KEY=<yourExchangeKey>
|
||||||
|
FREQTRADE__EXCHANGE__SECRET=<yourExchangeSecret>
|
||||||
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Environment variables detected are logged at startup - so if you can't find why a value is not what you think it should be based on the configuration, make sure it's not loaded from an environment variable.
|
Environment variables detected are logged at startup - so if you can't find why a value is not what you think it should be based on the configuration, make sure it's not loaded from an environment variable.
|
||||||
|
|
||||||
@ -93,6 +102,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy 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.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy 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.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell 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.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
|
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
|
||||||
|
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency sell is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
|
||||||
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
|
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
|
||||||
| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled).
|
| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled).
|
||||||
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
|
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
|
||||||
@ -116,9 +126,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `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
|
| `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
|
||||||
| `exchange.secret` | API secret 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
|
| `exchange.secret` | API secret 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
|
||||||
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
|
| `exchange.uid` | API uid to use for the exchange. Only required when you are in production mode and for exchanges that use uid for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
||||||
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
||||||
| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for additional ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation). Please avoid adding exchange secrets here (use the dedicated fields instead), as they may be contained in logs. <br> **Datatype:** Dict
|
||||||
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||||
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||||
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||||
@ -192,9 +203,8 @@ There are several methods to configure how much of the stake currency the bot wi
|
|||||||
#### Minimum trade stake
|
#### Minimum trade stake
|
||||||
|
|
||||||
The minimum stake amount will depend on exchange and pair and is usually listed in the exchange support pages.
|
The minimum stake amount will depend on exchange and pair and is usually listed in the exchange support pages.
|
||||||
Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.6$.
|
|
||||||
|
|
||||||
The minimum stake amount to buy this pair is, therefore, `20 * 0.6 ~= 12`.
|
Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.6$, the minimum stake amount to buy this pair is `20 * 0.6 ~= 12`.
|
||||||
This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case.
|
This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case.
|
||||||
|
|
||||||
To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%).
|
To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%).
|
||||||
@ -204,7 +214,7 @@ With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05)
|
|||||||
To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit.
|
To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange.
|
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange. Freqtrade adjusts the stake-amount to this value, unless it's > 30% more than the calculated/desired stake-amount - in which case the trade is rejected.
|
||||||
|
|
||||||
#### Tradable balance
|
#### Tradable balance
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ Otherwise `--exchange` becomes mandatory.
|
|||||||
You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used.
|
You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used.
|
||||||
|
|
||||||
!!! Tip "Tip: Updating existing data"
|
!!! Tip "Tip: Updating existing data"
|
||||||
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, do not use `--days` or `--timerange` parameters. Freqtrade will keep the available data and only download the missing data.
|
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, freqtrade will automatically calculate the data missing for the existing pairs and the download will occur from the latest available point until "now", neither --days or --timerange parameters are required. Freqtrade will keep the available data and only download the missing data.
|
||||||
If you are updating existing data after inserting new pairs that you have no data for, use `--new-pairs-days xx` parameter. Specified number of days will be downloaded for new pairs while old pairs will be updated with missing data only.
|
If you are updating existing data after inserting new pairs that you have no data for, use `--new-pairs-days xx` parameter. Specified number of days will be downloaded for new pairs while old pairs will be updated with missing data only.
|
||||||
If you use `--days xx` parameter alone - data for specified number of days will be downloaded for _all_ pairs. Be careful, if specified number of days is smaller than gap between now and last downloaded candle - freqtrade will delete all existing data to avoid gaps in candle data.
|
If you use `--days xx` parameter alone - data for specified number of days will be downloaded for _all_ pairs. Be careful, if specified number of days is smaller than gap between now and last downloaded candle - freqtrade will delete all existing data to avoid gaps in candle data.
|
||||||
|
|
||||||
|
@ -26,6 +26,8 @@ Alternatively (e.g. if your system is not supported by the setup.sh script), fol
|
|||||||
|
|
||||||
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
||||||
|
|
||||||
|
Before opening a pull request, please familiarize yourself with our [Contributing Guidelines](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md).
|
||||||
|
|
||||||
### Devcontainer setup
|
### Devcontainer setup
|
||||||
|
|
||||||
The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension.
|
The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension.
|
||||||
@ -250,7 +252,23 @@ Most exchanges supported by CCXT should work out of the box.
|
|||||||
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`.
|
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`.
|
||||||
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
|
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
|
||||||
|
|
||||||
Also try to use `freqtrade download-data` for an extended timerange and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded).
|
Also try to use `freqtrade download-data` for an extended timerange (multiple months) and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded).
|
||||||
|
|
||||||
|
These are prerequisites to have an exchange listed as either Supported or Community tested (listed on the homepage).
|
||||||
|
The below are "extras", which will make an exchange better (feature-complete) - but are not absolutely necessary for either of the 2 categories.
|
||||||
|
|
||||||
|
Additional tests / steps to complete:
|
||||||
|
|
||||||
|
* Verify data provided by `fetch_ohlcv()` - and eventually adjust `ohlcv_candle_limit` for this exchange
|
||||||
|
* Check L2 orderbook limit range (API documentation) - and eventually set as necessary
|
||||||
|
* Check if balance shows correctly (*)
|
||||||
|
* Create market order (*)
|
||||||
|
* Create limit order (*)
|
||||||
|
* Complete trade (buy + sell) (*)
|
||||||
|
* Compare result calculation between exchange and bot
|
||||||
|
* Ensure fees are applied correctly (check the database against the exchange)
|
||||||
|
|
||||||
|
(*) Requires API keys and Balance on the exchange.
|
||||||
|
|
||||||
### Stoploss On Exchange
|
### Stoploss On Exchange
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ In case of problems related to rate-limits (usually DDOS Exceptions in your logs
|
|||||||
```
|
```
|
||||||
|
|
||||||
This configuration enables kraken, as well as rate-limiting to avoid bans from the exchange.
|
This configuration enables kraken, as well as rate-limiting to avoid bans from the exchange.
|
||||||
`"rateLimit": 3100` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false.
|
`"rateLimit": 3100` defines a wait-event of 3.1s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
|
Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
|
||||||
@ -182,6 +182,23 @@ Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force)
|
|||||||
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
||||||
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
||||||
|
|
||||||
|
## OKEX
|
||||||
|
|
||||||
|
OKEX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"exchange": {
|
||||||
|
"name": "okex",
|
||||||
|
"key": "your_exchange_key",
|
||||||
|
"secret": "your_exchange_secret",
|
||||||
|
"password": "your_exchange_api_key_password",
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
OKEX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode.
|
||||||
|
|
||||||
## All exchanges
|
## All exchanges
|
||||||
|
|
||||||
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
||||||
|
29
docs/faq.md
29
docs/faq.md
@ -42,7 +42,7 @@ position for a trade. Be patient!
|
|||||||
### I have made 12 trades already, why is my total profit negative?
|
### I have made 12 trades already, why is my total profit negative?
|
||||||
|
|
||||||
I understand your disappointment but unfortunately 12 trades is just
|
I understand your disappointment but unfortunately 12 trades is just
|
||||||
not enough to say anything. If you run backtesting, you can see that our
|
not enough to say anything. If you run backtesting, you can see that the
|
||||||
current algorithm does leave you on the plus side, but that is after
|
current algorithm does leave you on the plus side, but that is after
|
||||||
thousands of trades and even there, you will be left with losses on
|
thousands of trades and even there, you will be left with losses on
|
||||||
specific coins that you have traded tens if not hundreds of times. We
|
specific coins that you have traded tens if not hundreds of times. We
|
||||||
@ -54,6 +54,21 @@ you can't say much from few trades.
|
|||||||
|
|
||||||
Yes. You can edit your config and use the `/reload_config` command to reload the configuration. The bot will stop, reload the configuration and strategy and will restart with the new configuration and strategy.
|
Yes. You can edit your config and use the `/reload_config` command to reload the configuration. The bot will stop, reload the configuration and strategy and will restart with the new configuration and strategy.
|
||||||
|
|
||||||
|
### Why does my bot not sell everything it bought?
|
||||||
|
|
||||||
|
This is called "coin dust" and can happen on all exchanges.
|
||||||
|
It happens because many exchanges subtract fees from the "receiving currency" - so you buy 100 COIN - but you only get 99.9 COIN.
|
||||||
|
As COIN is trading in full lot sizes (1COIN steps), you cannot sell 0.9 COIN (or 99.9 COIN) - but you need to round down to 99 COIN.
|
||||||
|
|
||||||
|
This is not a bot-problem, but will also happen while manual trading.
|
||||||
|
|
||||||
|
While freqtrade can handle this (it'll sell 99 COIN), fees are often below the minimum tradable lot-size (you can only trade full COIN, not 0.9 COIN).
|
||||||
|
Leaving the dust (0.9 COIN) on the exchange makes usually sense, as the next time freqtrade buys COIN, it'll eat into the remaining small balance, this time selling everything it bought, and therefore slowly declining the dust balance (although it most likely will never reach exactly 0).
|
||||||
|
|
||||||
|
Where possible (e.g. on binance), the use of the exchange's dedicated fee currency will fix this.
|
||||||
|
On binance, it's sufficient to have BNB in your account, and have "Pay fees in BNB" enabled in your profile. Your BNB balance will slowly decline (as it's used to pay fees) - but you'll no longer encounter dust (Freqtrade will include the fees in the profit calculations).
|
||||||
|
Other exchanges don't offer such possibilities, where it's simply something you'll have to accept or move to a different exchange.
|
||||||
|
|
||||||
### I want to use incomplete candles
|
### I want to use incomplete candles
|
||||||
|
|
||||||
Freqtrade will not provide incomplete candles to strategies. Using incomplete candles will lead to repainting and consequently to strategies with "ghost" buys, which are impossible to both backtest, and verify after they happened.
|
Freqtrade will not provide incomplete candles to strategies. Using incomplete candles will lead to repainting and consequently to strategies with "ghost" buys, which are impossible to both backtest, and verify after they happened.
|
||||||
@ -78,6 +93,18 @@ If this happens for all pairs in the pairlist, this might indicate a recent exch
|
|||||||
|
|
||||||
Irrespectively of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles.
|
Irrespectively of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles.
|
||||||
|
|
||||||
|
### I'm getting "Outdated history for pair xxx" in the log
|
||||||
|
|
||||||
|
The bot is trying to tell you that it got an outdated last candle (not the last complete candle).
|
||||||
|
As a consequence, Freqtrade will not enter a trade for this pair - as trading on old information is usually not what is desired.
|
||||||
|
|
||||||
|
This warning can point to one of the below problems:
|
||||||
|
|
||||||
|
* Exchange downtime -> Check your exchange status page / blog / twitter feed for details.
|
||||||
|
* Wrong system time -> Ensure your system-time is correct.
|
||||||
|
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
|
||||||
|
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
|
||||||
|
|
||||||
### I'm getting the "RESTRICTED_MARKET" message in the log
|
### I'm getting the "RESTRICTED_MARKET" message in the log
|
||||||
|
|
||||||
Currently known to happen for US Bittrex users.
|
Currently known to happen for US Bittrex users.
|
||||||
|
@ -116,7 +116,7 @@ optional arguments:
|
|||||||
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
|
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
|
||||||
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
|
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
|
||||||
SortinoHyperOptLoss, SortinoHyperOptLossDaily,
|
SortinoHyperOptLoss, SortinoHyperOptLossDaily,
|
||||||
MaxDrawDownHyperOptLoss
|
CalmarHyperOptLoss, MaxDrawDownHyperOptLoss
|
||||||
--disable-param-export
|
--disable-param-export
|
||||||
Disable automatic hyperopt parameter export.
|
Disable automatic hyperopt parameter export.
|
||||||
--ignore-missing-spaces, --ignore-unparameterized-spaces
|
--ignore-missing-spaces, --ignore-unparameterized-spaces
|
||||||
@ -524,6 +524,7 @@ Currently, the following loss functions are builtin:
|
|||||||
* `SortinoHyperOptLoss` - optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation.
|
* `SortinoHyperOptLoss` - optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation.
|
||||||
* `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation.
|
* `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation.
|
||||||
* `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown.
|
* `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown.
|
||||||
|
* `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown.
|
||||||
|
|
||||||
Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.
|
Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.
|
||||||
|
|
||||||
|
@ -52,6 +52,8 @@ To skip pair validation against active markets, set `"allow_inactive": true` wit
|
|||||||
This can be useful for backtesting expired pairs (like quarterly spot-markets).
|
This can be useful for backtesting expired pairs (like quarterly spot-markets).
|
||||||
This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration.
|
This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration.
|
||||||
|
|
||||||
|
When used in a "follow-up" position (e.g. after VolumePairlist), all pairs in `'pair_whitelist'` will be added to the end of the pairlist.
|
||||||
|
|
||||||
#### Volume Pair List
|
#### Volume Pair List
|
||||||
|
|
||||||
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
|
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
|
||||||
@ -196,7 +198,7 @@ Not defining this parameter (or setting it to 0) will use all-time performance.
|
|||||||
|
|
||||||
The optional `min_profit` parameter defines the minimum profit a pair must have to be considered.
|
The optional `min_profit` parameter defines the minimum profit a pair must have to be considered.
|
||||||
Pairs below this level will be filtered out.
|
Pairs below this level will be filtered out.
|
||||||
Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without without a way to recover.
|
Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without a way to recover.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"pairlists": [
|
"pairlists": [
|
||||||
@ -209,6 +211,8 @@ Using this parameter without `minutes` is highly discouraged, as it can lead to
|
|||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
|
As this Filter uses past performance of the bot, it'll have some startup-period - and should only be used after the bot has a few 100 trades in the database.
|
||||||
|
|
||||||
!!! Warning "Backtesting"
|
!!! Warning "Backtesting"
|
||||||
`PerformanceFilter` does not support backtesting mode.
|
`PerformanceFilter` does not support backtesting mode.
|
||||||
|
|
||||||
@ -216,6 +220,9 @@ Using this parameter without `minutes` is highly discouraged, as it can lead to
|
|||||||
|
|
||||||
Filters low-value coins which would not allow setting stoplosses.
|
Filters low-value coins which would not allow setting stoplosses.
|
||||||
|
|
||||||
|
!!! Warning "Backtesting"
|
||||||
|
`PrecisionFilter` does not support backtesting mode using multiple strategies.
|
||||||
|
|
||||||
#### PriceFilter
|
#### PriceFilter
|
||||||
|
|
||||||
The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
|
The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
|
||||||
@ -253,7 +260,7 @@ Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 -
|
|||||||
Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority.
|
Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority.
|
||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order.
|
You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order. ShuffleFilter will automatically detect runmodes and apply the `seed` only for backtesting modes - if a `seed` value is set.
|
||||||
|
|
||||||
#### SpreadFilter
|
#### SpreadFilter
|
||||||
|
|
||||||
@ -288,7 +295,7 @@ If the trading range over the last 10 days is <1% or >99%, remove the pair from
|
|||||||
|
|
||||||
#### VolatilityFilter
|
#### VolatilityFilter
|
||||||
|
|
||||||
Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)).
|
Volatility is the degree of historical variation of a pairs over time, it is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)).
|
||||||
|
|
||||||
This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
|
This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
|
||||||
|
|
||||||
@ -342,5 +349,5 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets,
|
|||||||
"refresh_period": 86400
|
"refresh_period": 86400
|
||||||
},
|
},
|
||||||
{"method": "ShuffleFilter", "seed": 42}
|
{"method": "ShuffleFilter", "seed": 42}
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
@ -36,11 +36,12 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
|
|||||||
|
|
||||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#binance-blacklist))
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
|
- [X] [Kraken](https://kraken.com/)
|
||||||
|
- [X] [OKEX](https://www.okex.com/)
|
||||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
@ -80,4 +81,4 @@ For any questions not covered by the documentation or for further information ab
|
|||||||
|
|
||||||
## Ready to try?
|
## Ready to try?
|
||||||
|
|
||||||
Begin by reading our installation guide [for docker](docker_quickstart.md) (recommended), or for [installation without docker](installation.md).
|
Begin by reading the installation guide [for docker](docker_quickstart.md) (recommended), or for [installation without docker](installation.md).
|
||||||
|
@ -60,7 +60,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
|
||||||
# install packages
|
# install packages
|
||||||
sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git
|
sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git curl
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "RaspberryPi/Raspbian"
|
=== "RaspberryPi/Raspbian"
|
||||||
@ -71,7 +71,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
|
|||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get install python3-venv libatlas-base-dev cmake
|
sudo apt-get install python3-venv libatlas-base-dev cmake curl
|
||||||
# Use pywheels.org to speed up installation
|
# Use pywheels.org to speed up installation
|
||||||
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf
|
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf
|
||||||
|
|
||||||
|
110
docs/plotting.md
110
docs/plotting.md
@ -164,16 +164,17 @@ The resulting plot will have the following elements:
|
|||||||
|
|
||||||
An advanced plot configuration can be specified in the strategy in the `plot_config` parameter.
|
An advanced plot configuration can be specified in the strategy in the `plot_config` parameter.
|
||||||
|
|
||||||
Additional features when using plot_config include:
|
Additional features when using `plot_config` include:
|
||||||
|
|
||||||
* Specify colors per indicator
|
* Specify colors per indicator
|
||||||
* Specify additional subplots
|
* Specify additional subplots
|
||||||
* Specify indicator pairs to fill area in between
|
* Specify indicator pairs to fill area in between
|
||||||
|
|
||||||
The sample plot configuration below specifies fixed colors for the indicators. Otherwise, consecutive plots may produce different color schemes each time, making comparisons difficult.
|
The sample plot configuration below specifies fixed colors for the indicators. Otherwise, consecutive plots may produce different color schemes each time, making comparisons difficult.
|
||||||
It also allows multiple subplots to display both MACD and RSI at the same time.
|
It also allows multiple subplots to display both MACD and RSI at the same time.
|
||||||
|
|
||||||
Plot type can be configured using `type` key. Possible types are:
|
Plot type can be configured using `type` key. Possible types are:
|
||||||
|
|
||||||
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
|
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
|
||||||
* `bar` corresponding to `plotly.graph_objects.Bar` class.
|
* `bar` corresponding to `plotly.graph_objects.Bar` class.
|
||||||
|
|
||||||
@ -182,40 +183,89 @@ Extra parameters to `plotly.graph_objects.*` constructor can be specified in `pl
|
|||||||
Sample configuration with inline comments explaining the process:
|
Sample configuration with inline comments explaining the process:
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
plot_config = {
|
@property
|
||||||
'main_plot': {
|
def plot_config(self):
|
||||||
# Configuration for main plot indicators.
|
"""
|
||||||
# Specifies `ema10` to be red, and `ema50` to be a shade of gray
|
There are a lot of solutions how to build the return dictionary.
|
||||||
'ema10': {'color': 'red'},
|
The only important point is the return value.
|
||||||
'ema50': {'color': '#CCCCCC'},
|
Example:
|
||||||
# By omitting color, a random color is selected.
|
plot_config = {'main_plot': {}, 'subplots': {}}
|
||||||
'sar': {},
|
|
||||||
# fill area between senkou_a and senkou_b
|
"""
|
||||||
'senkou_a': {
|
plot_config = {}
|
||||||
'color': 'green', #optional
|
plot_config['main_plot'] = {
|
||||||
'fill_to': 'senkou_b',
|
# Configuration for main plot indicators.
|
||||||
'fill_label': 'Ichimoku Cloud', #optional
|
# Assumes 2 parameters, emashort and emalong to be specified.
|
||||||
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
f'ema_{self.emashort.value}': {'color': 'red'},
|
||||||
},
|
f'ema_{self.emalong.value}': {'color': '#CCCCCC'},
|
||||||
# plot senkou_b, too. Not only the area to it.
|
# By omitting color, a random color is selected.
|
||||||
'senkou_b': {}
|
'sar': {},
|
||||||
|
# fill area between senkou_a and senkou_b
|
||||||
|
'senkou_a': {
|
||||||
|
'color': 'green', #optional
|
||||||
|
'fill_to': 'senkou_b',
|
||||||
|
'fill_label': 'Ichimoku Cloud', #optional
|
||||||
|
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
||||||
},
|
},
|
||||||
'subplots': {
|
# plot senkou_b, too. Not only the area to it.
|
||||||
# Create subplot MACD
|
'senkou_b': {}
|
||||||
"MACD": {
|
}
|
||||||
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
plot_config['subplots'] = {
|
||||||
'macdsignal': {'color': 'orange'},
|
# Create subplot MACD
|
||||||
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
"MACD": {
|
||||||
},
|
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
||||||
# Additional subplot RSI
|
'macdsignal': {'color': 'orange'},
|
||||||
"RSI": {
|
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
||||||
'rsi': {'color': 'red'}
|
},
|
||||||
}
|
# Additional subplot RSI
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return plot_config
|
||||||
```
|
```
|
||||||
|
|
||||||
|
??? Note "As attribute (former method)"
|
||||||
|
Assigning plot_config is also possible as Attribute (this used to be the default way).
|
||||||
|
This has the disadvantage that strategy parameters are not available, preventing certain configurations from working.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
plot_config = {
|
||||||
|
'main_plot': {
|
||||||
|
# Configuration for main plot indicators.
|
||||||
|
# Specifies `ema10` to be red, and `ema50` to be a shade of gray
|
||||||
|
'ema10': {'color': 'red'},
|
||||||
|
'ema50': {'color': '#CCCCCC'},
|
||||||
|
# By omitting color, a random color is selected.
|
||||||
|
'sar': {},
|
||||||
|
# fill area between senkou_a and senkou_b
|
||||||
|
'senkou_a': {
|
||||||
|
'color': 'green', #optional
|
||||||
|
'fill_to': 'senkou_b',
|
||||||
|
'fill_label': 'Ichimoku Cloud', #optional
|
||||||
|
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
||||||
|
},
|
||||||
|
# plot senkou_b, too. Not only the area to it.
|
||||||
|
'senkou_b': {}
|
||||||
|
},
|
||||||
|
'subplots': {
|
||||||
|
# Create subplot MACD
|
||||||
|
"MACD": {
|
||||||
|
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
||||||
|
'macdsignal': {'color': 'orange'},
|
||||||
|
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
||||||
|
},
|
||||||
|
# Additional subplot RSI
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
|
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
|
||||||
`macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy.
|
`macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mkdocs==1.2.3
|
mkdocs==1.2.3
|
||||||
mkdocs-material==7.3.4
|
mkdocs-material==8.0.1
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==9.0
|
pymdown-extensions==9.1
|
||||||
|
@ -38,6 +38,11 @@ Sample configuration:
|
|||||||
!!! Danger "Security warning"
|
!!! Danger "Security warning"
|
||||||
By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot.
|
By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot.
|
||||||
|
|
||||||
|
??? Note "API/UI Access on a remote servers"
|
||||||
|
If you're running on a VPS, you should consider using either a ssh tunnel, or setup a VPN (openVPN, wireguard) to connect to your bot.
|
||||||
|
This will ensure that freqUI is not directly exposed to the internet, which is not recommended for security reasons (freqUI does not support https out of the box).
|
||||||
|
Setup of these tools is not part of this tutorial, however many good tutorials can be found on the internet.
|
||||||
|
|
||||||
You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly.
|
You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly.
|
||||||
This should return the response:
|
This should return the response:
|
||||||
|
|
||||||
@ -330,12 +335,15 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques
|
|||||||
|
|
||||||
### CORS
|
### CORS
|
||||||
|
|
||||||
All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
|
This whole section is only necessary in cross-origin cases (where you multiple bot API's running on `localhost:8081`, `localhost:8082`, ...), and want to combine them into one FreqUI instance.
|
||||||
Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems.
|
|
||||||
Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately.
|
|
||||||
|
|
||||||
Users can configure this themselves via the `CORS_origins` configuration setting.
|
??? info "Technical explanation"
|
||||||
It consists of a list of allowed sites that are allowed to consume resources from the bot's API.
|
All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
|
||||||
|
Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems.
|
||||||
|
Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately.
|
||||||
|
|
||||||
|
Users can allow access from different origin URL's to the bot API via the `CORS_origins` configuration setting.
|
||||||
|
It consists of a list of allowed URL's that are allowed to consume resources from the bot's API.
|
||||||
|
|
||||||
Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary:
|
Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary:
|
||||||
|
|
||||||
@ -348,5 +356,19 @@ Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - t
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In the following (pretty common) case, FreqUI is accessible on `http://localhost:8080/trade` (this is what you see in your navbar when navigating to freqUI).
|
||||||
|
![freqUI url](assets/frequi_url.png)
|
||||||
|
|
||||||
|
The correct configuration for this case is `http://localhost:8080` - the main part of the URL including the port.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
//...
|
||||||
|
"jwt_secret_key": "somethingrandom",
|
||||||
|
"CORS_origins": ["http://localhost:8080"],
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot.
|
We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot.
|
||||||
|
@ -182,7 +182,7 @@ For example, simplified math:
|
|||||||
* the bot buys an asset at a price of 100$
|
* the bot buys an asset at a price of 100$
|
||||||
* the stop loss is defined at -10%
|
* the stop loss is defined at -10%
|
||||||
* the stop loss would get triggered once the asset drops below 90$
|
* the stop loss would get triggered once the asset drops below 90$
|
||||||
* stoploss will remain at 90$ unless asset increases to or above our configured offset
|
* stoploss will remain at 90$ unless asset increases to or above the configured offset
|
||||||
* assuming the asset now increases to 103$ (where we have the offset configured)
|
* assuming the asset now increases to 103$ (where we have the offset configured)
|
||||||
* the stop loss will now be -2% of 103$ = 100.94$
|
* the stop loss will now be -2% of 103$ = 100.94$
|
||||||
* now the asset drops in value to 101\$, the stop loss will still be 100.94$ and would trigger at 100.94$
|
* now the asset drops in value to 101\$, the stop loss will still be 100.94$ and would trigger at 100.94$
|
||||||
|
@ -77,43 +77,6 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
## Custom sell signal
|
|
||||||
|
|
||||||
It is possible to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision.
|
|
||||||
|
|
||||||
For example you could implement a 1:2 risk-reward ROI with `custom_sell()`.
|
|
||||||
|
|
||||||
Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
|
|
||||||
|
|
||||||
An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day:
|
|
||||||
|
|
||||||
``` python
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
|
||||||
current_profit: float, **kwargs):
|
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
|
||||||
last_candle = dataframe.iloc[-1].squeeze()
|
|
||||||
|
|
||||||
# Above 20% profit, sell when rsi < 80
|
|
||||||
if current_profit > 0.2:
|
|
||||||
if last_candle['rsi'] < 80:
|
|
||||||
return 'rsi_below_80'
|
|
||||||
|
|
||||||
# Between 2% and 10%, sell if EMA-long above EMA-short
|
|
||||||
if 0.02 < current_profit < 0.1:
|
|
||||||
if last_candle['emalong'] > last_candle['emashort']:
|
|
||||||
return 'ema_long_below_80'
|
|
||||||
|
|
||||||
# Sell any positions at a loss if they are held for more than one day.
|
|
||||||
if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1:
|
|
||||||
return 'unclog'
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
|
|
||||||
|
|
||||||
## Buy Tag
|
## Buy Tag
|
||||||
|
|
||||||
When your strategy has multiple buy signals, you can name the signal that triggered.
|
When your strategy has multiple buy signals, you can name the signal that triggered.
|
||||||
@ -143,506 +106,26 @@ def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_r
|
|||||||
!!! Note
|
!!! Note
|
||||||
`buy_tag` is limited to 100 characters, remaining data will be truncated.
|
`buy_tag` is limited to 100 characters, remaining data will be truncated.
|
||||||
|
|
||||||
|
## Exit tag
|
||||||
|
|
||||||
## Custom stoploss
|
Similar to [Buy Tagging](#buy-tag), you can also specify a sell tag.
|
||||||
|
|
||||||
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss.
|
|
||||||
|
|
||||||
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
|
|
||||||
The method must return a stoploss value (float / number) as a percentage of the current price.
|
|
||||||
E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD.
|
|
||||||
|
|
||||||
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
|
|
||||||
|
|
||||||
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
|
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
# additional imports required
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
from datetime import datetime
|
dataframe.loc[
|
||||||
from freqtrade.persistence import Trade
|
(
|
||||||
|
(dataframe['rsi'] > 70) &
|
||||||
|
(dataframe['volume'] > 0)
|
||||||
|
),
|
||||||
|
['sell', 'exit_tag']] = (1, 'exit_rsi')
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
return dataframe
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
"""
|
|
||||||
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
|
||||||
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
|
||||||
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
|
||||||
|
|
||||||
When not implemented by a strategy, returns the initial stoploss value
|
|
||||||
Only called when use_custom_stoploss is set to True.
|
|
||||||
|
|
||||||
:param pair: Pair that's currently analyzed
|
|
||||||
:param trade: trade object.
|
|
||||||
:param current_time: datetime object, containing the current datetime
|
|
||||||
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
|
||||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
|
||||||
:return float: New stoploss value, relative to the current rate
|
|
||||||
"""
|
|
||||||
return -0.04
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)).
|
The provided exit-tag is then used as sell-reason - and shown as such in backtest results.
|
||||||
|
|
||||||
!!! Note "Use of dates"
|
|
||||||
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
|
|
||||||
|
|
||||||
!!! Tip "Trailing stoploss"
|
|
||||||
It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior.
|
|
||||||
|
|
||||||
### Custom stoploss examples
|
|
||||||
|
|
||||||
The next section will show some examples on what's possible with the custom stoploss function.
|
|
||||||
Of course, many more things are possible, and all examples can be combined at will.
|
|
||||||
|
|
||||||
#### Time based trailing stop
|
|
||||||
|
|
||||||
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
|
||||||
if current_time - timedelta(minutes=120) > trade.open_date_utc:
|
|
||||||
return -0.05
|
|
||||||
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
|
|
||||||
return -0.10
|
|
||||||
return 1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Different stoploss per pair
|
|
||||||
|
|
||||||
Use a different stoploss depending on the pair.
|
|
||||||
In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
if pair in ('ETH/BTC', 'XRP/BTC'):
|
|
||||||
return -0.10
|
|
||||||
elif pair in ('LTC/BTC'):
|
|
||||||
return -0.05
|
|
||||||
return -0.15
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Trailing stoploss with positive offset
|
|
||||||
|
|
||||||
Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%.
|
|
||||||
|
|
||||||
Please note that the stoploss can only increase, values lower than the current stoploss are ignored.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
if current_profit < 0.04:
|
|
||||||
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
|
|
||||||
|
|
||||||
# After reaching the desired offset, allow the stoploss to trail by half the profit
|
|
||||||
desired_stoploss = current_profit / 2
|
|
||||||
|
|
||||||
# Use a minimum of 2.5% and a maximum of 5%
|
|
||||||
return max(min(desired_stoploss, 0.05), 0.025)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Calculating stoploss relative to open price
|
|
||||||
|
|
||||||
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
|
||||||
|
|
||||||
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
|
||||||
|
|
||||||
### Calculating stoploss percentage from absolute price
|
|
||||||
|
|
||||||
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
|
||||||
|
|
||||||
The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
|
|
||||||
|
|
||||||
#### Stepped stoploss
|
|
||||||
|
|
||||||
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
|
||||||
|
|
||||||
* Use the regular stoploss until 20% profit is reached
|
|
||||||
* Once profit is > 20% - set stoploss to 7% above open price.
|
|
||||||
* Once profit is > 25% - set stoploss to 15% above open price.
|
|
||||||
* Once profit is > 40% - set stoploss to 25% above open price.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
from freqtrade.strategy import stoploss_from_open
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
# evaluate highest to lowest, so that highest possible stop is used
|
|
||||||
if current_profit > 0.40:
|
|
||||||
return stoploss_from_open(0.25, current_profit)
|
|
||||||
elif current_profit > 0.25:
|
|
||||||
return stoploss_from_open(0.15, current_profit)
|
|
||||||
elif current_profit > 0.20:
|
|
||||||
return stoploss_from_open(0.07, current_profit)
|
|
||||||
|
|
||||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
|
||||||
return 1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Custom stoploss using an indicator from dataframe example
|
|
||||||
|
|
||||||
Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
# <...>
|
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
|
||||||
last_candle = dataframe.iloc[-1].squeeze()
|
|
||||||
|
|
||||||
# Use parabolic sar as absolute stoploss price
|
|
||||||
stoploss_price = last_candle['sar']
|
|
||||||
|
|
||||||
# Convert absolute price to percentage relative to current_rate
|
|
||||||
if stoploss_price < current_rate:
|
|
||||||
return (stoploss_price / current_rate) - 1
|
|
||||||
|
|
||||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
|
||||||
return 1
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Custom order price rules
|
|
||||||
|
|
||||||
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
|
||||||
|
|
||||||
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
|
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
`sell_reason` is limited to 100 characters, remaining data will be truncated.
|
||||||
|
|
||||||
### Custom order entry and exit price example
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
def custom_entry_price(self, pair: str, current_time: datetime,
|
|
||||||
proposed_rate, **kwargs) -> float:
|
|
||||||
|
|
||||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
|
||||||
timeframe=self.timeframe)
|
|
||||||
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
|
|
||||||
|
|
||||||
return new_entryprice
|
|
||||||
|
|
||||||
def custom_exit_price(self, pair: str, trade: Trade,
|
|
||||||
current_time: datetime, proposed_rate: float,
|
|
||||||
current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
|
||||||
timeframe=self.timeframe)
|
|
||||||
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
|
|
||||||
|
|
||||||
return new_exitprice
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Warning
|
|
||||||
Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter.
|
|
||||||
|
|
||||||
!!! Example
|
|
||||||
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98.
|
|
||||||
|
|
||||||
!!! Warning "No backtesting support"
|
|
||||||
Custom entry-prices are currently not supported during backtesting.
|
|
||||||
|
|
||||||
## Custom order timeout rules
|
|
||||||
|
|
||||||
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
|
|
||||||
|
|
||||||
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
|
|
||||||
|
|
||||||
### Custom order timeout example
|
|
||||||
|
|
||||||
A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
|
|
||||||
It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
|
|
||||||
|
|
||||||
The function must return either `True` (cancel order) or `False` (keep order alive).
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
# Set unfilledtimeout to 25 hours, since our maximum timeout from below is 24 hours.
|
|
||||||
unfilledtimeout = {
|
|
||||||
'buy': 60 * 25,
|
|
||||||
'sell': 60 * 25
|
|
||||||
}
|
|
||||||
|
|
||||||
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
|
|
||||||
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
|
|
||||||
return True
|
|
||||||
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
|
|
||||||
return True
|
|
||||||
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
|
|
||||||
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
|
|
||||||
return True
|
|
||||||
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
|
|
||||||
return True
|
|
||||||
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
For the above example, `unfilledtimeout` must be set to something bigger than 24h, otherwise that type of timeout will apply first.
|
|
||||||
|
|
||||||
### Custom order timeout example (using additional data)
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
# Set unfilledtimeout to 25 hours, since our maximum timeout from below is 24 hours.
|
|
||||||
unfilledtimeout = {
|
|
||||||
'buy': 60 * 25,
|
|
||||||
'sell': 60 * 25
|
|
||||||
}
|
|
||||||
|
|
||||||
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
|
||||||
ob = self.dp.orderbook(pair, 1)
|
|
||||||
current_price = ob['bids'][0][0]
|
|
||||||
# Cancel buy order if price is more than 2% above the order.
|
|
||||||
if current_price > order['price'] * 1.02:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
|
||||||
ob = self.dp.orderbook(pair, 1)
|
|
||||||
current_price = ob['asks'][0][0]
|
|
||||||
# Cancel sell order if price is more than 2% below the order.
|
|
||||||
if current_price < order['price'] * 0.98:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bot loop start callback
|
|
||||||
|
|
||||||
A simple callback which is called once at the start of every bot throttling iteration.
|
|
||||||
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
import requests
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
def bot_loop_start(self, **kwargs) -> None:
|
|
||||||
"""
|
|
||||||
Called at the start of the bot iteration (one loop).
|
|
||||||
Might be used to perform pair-independent tasks
|
|
||||||
(e.g. gather some remote resource for comparison)
|
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
|
||||||
"""
|
|
||||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
|
||||||
# Assign this to the class by using self.*
|
|
||||||
# can then be used by populate_* methods
|
|
||||||
self.remote_data = requests.get('https://some_remote_source.example.com')
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Bot order confirmation
|
|
||||||
|
|
||||||
### Trade entry (buy order) confirmation
|
|
||||||
|
|
||||||
`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect).
|
|
||||||
|
|
||||||
``` python
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
|
||||||
time_in_force: str, current_time: datetime, **kwargs) -> bool:
|
|
||||||
"""
|
|
||||||
Called right before placing a buy order.
|
|
||||||
Timing for this function is critical, so avoid doing heavy computations or
|
|
||||||
network requests in this method.
|
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
|
||||||
|
|
||||||
When not implemented by a strategy, returns True (always confirming).
|
|
||||||
|
|
||||||
:param pair: Pair that's about to be bought.
|
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
|
||||||
:param amount: Amount in target (quote) currency that's going to be traded.
|
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
|
||||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
|
||||||
:param current_time: datetime object, containing the current datetime
|
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
|
||||||
:return bool: When True is returned, then the buy-order is placed on the exchange.
|
|
||||||
False aborts the process
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Trade exit (sell order) confirmation
|
|
||||||
|
|
||||||
`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect).
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
|
||||||
rate: float, time_in_force: str, sell_reason: str,
|
|
||||||
current_time: datetime, **kwargs) -> bool:
|
|
||||||
"""
|
|
||||||
Called right before placing a regular sell order.
|
|
||||||
Timing for this function is critical, so avoid doing heavy computations or
|
|
||||||
network requests in this method.
|
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
|
||||||
|
|
||||||
When not implemented by a strategy, returns True (always confirming).
|
|
||||||
|
|
||||||
:param pair: Pair that's about to be sold.
|
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
|
||||||
:param amount: Amount in quote currency.
|
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
|
||||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
|
||||||
:param sell_reason: Sell reason.
|
|
||||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
|
||||||
'sell_signal', 'force_sell', 'emergency_sell']
|
|
||||||
:param current_time: datetime object, containing the current datetime
|
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
|
||||||
:return bool: When True is returned, then the sell-order is placed on the exchange.
|
|
||||||
False aborts the process
|
|
||||||
"""
|
|
||||||
if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0:
|
|
||||||
# Reject force-sells with negative profit
|
|
||||||
# This is just a sample, please adjust to your needs
|
|
||||||
# (this does not necessarily make sense, assuming you know when you're force-selling)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stake size management
|
|
||||||
|
|
||||||
It is possible to manage your risk by reducing or increasing stake amount when placing a new trade.
|
|
||||||
|
|
||||||
```python
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
|
||||||
proposed_stake: float, min_stake: float, max_stake: float,
|
|
||||||
**kwargs) -> float:
|
|
||||||
|
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
|
|
||||||
current_candle = dataframe.iloc[-1].squeeze()
|
|
||||||
|
|
||||||
if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']:
|
|
||||||
if self.config['stake_amount'] == 'unlimited':
|
|
||||||
# Use entire available wallet during favorable conditions when in compounding mode.
|
|
||||||
return max_stake
|
|
||||||
else:
|
|
||||||
# Compound profits during favorable conditions instead of using a static stake.
|
|
||||||
return self.wallets.get_total_stake_amount() / self.config['max_open_trades']
|
|
||||||
|
|
||||||
# Use default stake amount.
|
|
||||||
return proposed_stake
|
|
||||||
```
|
|
||||||
|
|
||||||
Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged.
|
|
||||||
|
|
||||||
!!! Tip
|
|
||||||
You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this acton will be logged.
|
|
||||||
|
|
||||||
!!! Tip
|
|
||||||
Returning `0` or `None` will prevent trades from being placed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Derived strategies
|
## Derived strategies
|
||||||
|
|
||||||
|
568
docs/strategy-callbacks.md
Normal file
568
docs/strategy-callbacks.md
Normal file
@ -0,0 +1,568 @@
|
|||||||
|
# Strategy Callbacks
|
||||||
|
|
||||||
|
While the main strategy functions (`populate_indicators()`, `populate_buy_trend()`, `populate_sell_trend()`) should be used in a vectorized way, and are only called [once during backtesting](bot-basics.md#backtesting-hyperopt-execution-logic), callbacks are called "whenever needed".
|
||||||
|
|
||||||
|
As such, you should avoid doing heavy calculations in callbacks to avoid delays during operations.
|
||||||
|
Depending on the callback used, they may be called when entering / exiting a trade, or throughout the duration of a trade.
|
||||||
|
|
||||||
|
Currently available callbacks:
|
||||||
|
|
||||||
|
* [`bot_loop_start()`](#bot-loop-start)
|
||||||
|
* [`custom_stake_amount()`](#custom-stake-size)
|
||||||
|
* [`custom_sell()`](#custom-sell-signal)
|
||||||
|
* [`custom_stoploss()`](#custom-stoploss)
|
||||||
|
* [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules)
|
||||||
|
* [`check_buy_timeout()` and `check_sell_timeout()](#custom-order-timeout-rules)
|
||||||
|
* [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation)
|
||||||
|
* [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation)
|
||||||
|
|
||||||
|
!!! Tip "Callback calling sequence"
|
||||||
|
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
|
||||||
|
|
||||||
|
## Bot loop start
|
||||||
|
|
||||||
|
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
|
||||||
|
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
def bot_loop_start(self, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Called at the start of the bot iteration (one loop).
|
||||||
|
Might be used to perform pair-independent tasks
|
||||||
|
(e.g. gather some remote resource for comparison)
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
"""
|
||||||
|
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||||
|
# Assign this to the class by using self.*
|
||||||
|
# can then be used by populate_* methods
|
||||||
|
self.remote_data = requests.get('https://some_remote_source.example.com')
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Stake size
|
||||||
|
|
||||||
|
Called before entering a trade, makes it possible to manage your position size when placing a new trade.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
|
proposed_stake: float, min_stake: float, max_stake: float,
|
||||||
|
**kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
|
||||||
|
current_candle = dataframe.iloc[-1].squeeze()
|
||||||
|
|
||||||
|
if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']:
|
||||||
|
if self.config['stake_amount'] == 'unlimited':
|
||||||
|
# Use entire available wallet during favorable conditions when in compounding mode.
|
||||||
|
return max_stake
|
||||||
|
else:
|
||||||
|
# Compound profits during favorable conditions instead of using a static stake.
|
||||||
|
return self.wallets.get_total_stake_amount() / self.config['max_open_trades']
|
||||||
|
|
||||||
|
# Use default stake amount.
|
||||||
|
return proposed_stake
|
||||||
|
```
|
||||||
|
|
||||||
|
Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged.
|
||||||
|
|
||||||
|
!!! Tip
|
||||||
|
You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this acton will be logged.
|
||||||
|
|
||||||
|
!!! Tip
|
||||||
|
Returning `0` or `None` will prevent trades from being placed.
|
||||||
|
|
||||||
|
## Custom sell signal
|
||||||
|
|
||||||
|
Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed.
|
||||||
|
|
||||||
|
Allows to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need trade data to make an exit decision.
|
||||||
|
|
||||||
|
For example you could implement a 1:2 risk-reward ROI with `custom_sell()`.
|
||||||
|
|
||||||
|
Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Returning a (none-empty) `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
|
||||||
|
|
||||||
|
An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
||||||
|
current_profit: float, **kwargs):
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
last_candle = dataframe.iloc[-1].squeeze()
|
||||||
|
|
||||||
|
# Above 20% profit, sell when rsi < 80
|
||||||
|
if current_profit > 0.2:
|
||||||
|
if last_candle['rsi'] < 80:
|
||||||
|
return 'rsi_below_80'
|
||||||
|
|
||||||
|
# Between 2% and 10%, sell if EMA-long above EMA-short
|
||||||
|
if 0.02 < current_profit < 0.1:
|
||||||
|
if last_candle['emalong'] > last_candle['emashort']:
|
||||||
|
return 'ema_long_below_80'
|
||||||
|
|
||||||
|
# Sell any positions at a loss if they are held for more than one day.
|
||||||
|
if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1:
|
||||||
|
return 'unclog'
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks.
|
||||||
|
|
||||||
|
## Custom stoploss
|
||||||
|
|
||||||
|
Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed.
|
||||||
|
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
|
||||||
|
|
||||||
|
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss (before this method is called for the first time for a trade).
|
||||||
|
|
||||||
|
The method must return a stoploss value (float / number) as a percentage of the current price.
|
||||||
|
E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD.
|
||||||
|
|
||||||
|
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
|
||||||
|
|
||||||
|
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
# additional imports required
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
"""
|
||||||
|
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
||||||
|
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||||
|
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
||||||
|
|
||||||
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
|
When not implemented by a strategy, returns the initial stoploss value
|
||||||
|
Only called when use_custom_stoploss is set to True.
|
||||||
|
|
||||||
|
:param pair: Pair that's currently analyzed
|
||||||
|
:param trade: trade object.
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||||
|
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return float: New stoploss value, relative to the current rate
|
||||||
|
"""
|
||||||
|
return -0.04
|
||||||
|
```
|
||||||
|
|
||||||
|
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)).
|
||||||
|
|
||||||
|
!!! Note "Use of dates"
|
||||||
|
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
|
||||||
|
|
||||||
|
!!! Tip "Trailing stoploss"
|
||||||
|
It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior.
|
||||||
|
|
||||||
|
### Custom stoploss examples
|
||||||
|
|
||||||
|
The next section will show some examples on what's possible with the custom stoploss function.
|
||||||
|
Of course, many more things are possible, and all examples can be combined at will.
|
||||||
|
|
||||||
|
#### Time based trailing stop
|
||||||
|
|
||||||
|
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
||||||
|
if current_time - timedelta(minutes=120) > trade.open_date_utc:
|
||||||
|
return -0.05
|
||||||
|
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
|
||||||
|
return -0.10
|
||||||
|
return 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Different stoploss per pair
|
||||||
|
|
||||||
|
Use a different stoploss depending on the pair.
|
||||||
|
In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
if pair in ('ETH/BTC', 'XRP/BTC'):
|
||||||
|
return -0.10
|
||||||
|
elif pair in ('LTC/BTC'):
|
||||||
|
return -0.05
|
||||||
|
return -0.15
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Trailing stoploss with positive offset
|
||||||
|
|
||||||
|
Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%.
|
||||||
|
|
||||||
|
Please note that the stoploss can only increase, values lower than the current stoploss are ignored.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
if current_profit < 0.04:
|
||||||
|
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
|
||||||
|
|
||||||
|
# After reaching the desired offset, allow the stoploss to trail by half the profit
|
||||||
|
desired_stoploss = current_profit / 2
|
||||||
|
|
||||||
|
# Use a minimum of 2.5% and a maximum of 5%
|
||||||
|
return max(min(desired_stoploss, 0.05), 0.025)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Stepped stoploss
|
||||||
|
|
||||||
|
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
||||||
|
|
||||||
|
* Use the regular stoploss until 20% profit is reached
|
||||||
|
* Once profit is > 20% - set stoploss to 7% above open price.
|
||||||
|
* Once profit is > 25% - set stoploss to 15% above open price.
|
||||||
|
* Once profit is > 40% - set stoploss to 25% above open price.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import stoploss_from_open
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
# evaluate highest to lowest, so that highest possible stop is used
|
||||||
|
if current_profit > 0.40:
|
||||||
|
return stoploss_from_open(0.25, current_profit)
|
||||||
|
elif current_profit > 0.25:
|
||||||
|
return stoploss_from_open(0.15, current_profit)
|
||||||
|
elif current_profit > 0.20:
|
||||||
|
return stoploss_from_open(0.07, current_profit)
|
||||||
|
|
||||||
|
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||||
|
return 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom stoploss using an indicator from dataframe example
|
||||||
|
|
||||||
|
Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# <...>
|
||||||
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
last_candle = dataframe.iloc[-1].squeeze()
|
||||||
|
|
||||||
|
# Use parabolic sar as absolute stoploss price
|
||||||
|
stoploss_price = last_candle['sar']
|
||||||
|
|
||||||
|
# Convert absolute price to percentage relative to current_rate
|
||||||
|
if stoploss_price < current_rate:
|
||||||
|
return (stoploss_price / current_rate) - 1
|
||||||
|
|
||||||
|
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||||
|
return 1
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks.
|
||||||
|
|
||||||
|
### Common helpers for stoploss calculations
|
||||||
|
|
||||||
|
#### Stoploss relative to open price
|
||||||
|
|
||||||
|
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||||
|
|
||||||
|
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||||
|
|
||||||
|
#### Stoploss percentage from absolute price
|
||||||
|
|
||||||
|
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||||
|
|
||||||
|
The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Custom order price rules
|
||||||
|
|
||||||
|
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
||||||
|
|
||||||
|
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
|
||||||
|
|
||||||
|
Each of these methods are called right before placing an order on the exchange.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
||||||
|
|
||||||
|
### Custom order entry and exit price example
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
def custom_entry_price(self, pair: str, current_time: datetime,
|
||||||
|
proposed_rate, **kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||||
|
timeframe=self.timeframe)
|
||||||
|
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
|
||||||
|
|
||||||
|
return new_entryprice
|
||||||
|
|
||||||
|
def custom_exit_price(self, pair: str, trade: Trade,
|
||||||
|
current_time: datetime, proposed_rate: float,
|
||||||
|
current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||||
|
timeframe=self.timeframe)
|
||||||
|
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
|
||||||
|
|
||||||
|
return new_exitprice
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter.
|
||||||
|
**Example**:
|
||||||
|
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate.
|
||||||
|
|
||||||
|
!!! Warning "No backtesting support"
|
||||||
|
Custom entry-prices are currently not supported during backtesting.
|
||||||
|
|
||||||
|
## Custom order timeout rules
|
||||||
|
|
||||||
|
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
|
||||||
|
|
||||||
|
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
|
||||||
|
|
||||||
|
### Custom order timeout example
|
||||||
|
|
||||||
|
Called for every open order until that order is either filled or cancelled.
|
||||||
|
`check_buy_timeout()` is called for trade entries, while `check_sell_timeout()` is called for trade exit orders.
|
||||||
|
|
||||||
|
A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
|
||||||
|
It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
|
||||||
|
|
||||||
|
The function must return either `True` (cancel order) or `False` (keep order alive).
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
|
||||||
|
unfilledtimeout = {
|
||||||
|
'buy': 60 * 25,
|
||||||
|
'sell': 60 * 25
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
|
||||||
|
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
|
||||||
|
return True
|
||||||
|
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
|
||||||
|
return True
|
||||||
|
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
|
||||||
|
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
|
||||||
|
return True
|
||||||
|
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
|
||||||
|
return True
|
||||||
|
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
For the above example, `unfilledtimeout` must be set to something bigger than 24h, otherwise that type of timeout will apply first.
|
||||||
|
|
||||||
|
### Custom order timeout example (using additional data)
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
|
||||||
|
unfilledtimeout = {
|
||||||
|
'buy': 60 * 25,
|
||||||
|
'sell': 60 * 25
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
||||||
|
ob = self.dp.orderbook(pair, 1)
|
||||||
|
current_price = ob['bids'][0][0]
|
||||||
|
# Cancel buy order if price is more than 2% above the order.
|
||||||
|
if current_price > order['price'] * 1.02:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
||||||
|
ob = self.dp.orderbook(pair, 1)
|
||||||
|
current_price = ob['asks'][0][0]
|
||||||
|
# Cancel sell order if price is more than 2% below the order.
|
||||||
|
if current_price < order['price'] * 0.98:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bot order confirmation
|
||||||
|
|
||||||
|
Confirm trade entry / exits.
|
||||||
|
This are the last methods that will be called before an order is placed.
|
||||||
|
|
||||||
|
### Trade entry (buy order) confirmation
|
||||||
|
|
||||||
|
`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect).
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||||
|
time_in_force: str, current_time: datetime, **kwargs) -> bool:
|
||||||
|
"""
|
||||||
|
Called right before placing a buy order.
|
||||||
|
Timing for this function is critical, so avoid doing heavy computations or
|
||||||
|
network requests in this method.
|
||||||
|
|
||||||
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
|
When not implemented by a strategy, returns True (always confirming).
|
||||||
|
|
||||||
|
:param pair: Pair that's about to be bought.
|
||||||
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
|
:param amount: Amount in target (quote) currency that's going to be traded.
|
||||||
|
:param rate: Rate that's going to be used when using limit orders
|
||||||
|
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return bool: When True is returned, then the buy-order is placed on the exchange.
|
||||||
|
False aborts the process
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trade exit (sell order) confirmation
|
||||||
|
|
||||||
|
`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect).
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
||||||
|
rate: float, time_in_force: str, sell_reason: str,
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
|
"""
|
||||||
|
Called right before placing a regular sell order.
|
||||||
|
Timing for this function is critical, so avoid doing heavy computations or
|
||||||
|
network requests in this method.
|
||||||
|
|
||||||
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
|
When not implemented by a strategy, returns True (always confirming).
|
||||||
|
|
||||||
|
:param pair: Pair that's about to be sold.
|
||||||
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
|
:param amount: Amount in quote currency.
|
||||||
|
:param rate: Rate that's going to be used when using limit orders
|
||||||
|
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
|
:param sell_reason: Sell reason.
|
||||||
|
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||||
|
'sell_signal', 'force_sell', 'emergency_sell']
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return bool: When True is returned, then the sell-order is placed on the exchange.
|
||||||
|
False aborts the process
|
||||||
|
"""
|
||||||
|
if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0:
|
||||||
|
# Reject force-sells with negative profit
|
||||||
|
# This is just a sample, please adjust to your needs
|
||||||
|
# (this does not necessarily make sense, assuming you know when you're force-selling)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
```
|
@ -4,33 +4,23 @@ This page explains how to customize your strategies, add new indicators and set
|
|||||||
|
|
||||||
Please familiarize yourself with [Freqtrade basics](bot-basics.md) first, which provides overall info on how the bot operates.
|
Please familiarize yourself with [Freqtrade basics](bot-basics.md) first, which provides overall info on how the bot operates.
|
||||||
|
|
||||||
## Install a custom strategy file
|
|
||||||
|
|
||||||
This is very simple. Copy paste your strategy file into the directory `user_data/strategies`.
|
|
||||||
|
|
||||||
Let assume you have a class called `AwesomeStrategy` in the file `AwesomeStrategy.py`:
|
|
||||||
|
|
||||||
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/AwesomeStrategy.py`
|
|
||||||
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade trade --strategy AwesomeStrategy
|
|
||||||
```
|
|
||||||
|
|
||||||
## Develop your own strategy
|
## Develop your own strategy
|
||||||
|
|
||||||
The bot includes a default strategy file.
|
The bot includes a default strategy file.
|
||||||
Also, several other strategies are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies).
|
Also, several other strategies are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies).
|
||||||
|
|
||||||
You will however most likely have your own idea for a strategy.
|
You will however most likely have your own idea for a strategy.
|
||||||
This document intends to help you develop one for yourself.
|
This document intends to help you convert your strategy idea into your own strategy.
|
||||||
|
|
||||||
To get started, use `freqtrade new-strategy --strategy AwesomeStrategy`.
|
To get started, use `freqtrade new-strategy --strategy AwesomeStrategy` (you can obviously use your own naming for your strategy).
|
||||||
This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`.
|
This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This is just a template file, which will most likely not be profitable out of the box.
|
This is just a template file, which will most likely not be profitable out of the box.
|
||||||
|
|
||||||
|
??? Hint "Different template levels"
|
||||||
|
`freqtrade new-strategy` has an additional parameter, `--template`, which controls the amount of pre-build information you get in the created strategy. Use `--template minimal` to get an empty strategy without any indicator examples, or `--template advanced` to get a template with most callbacks defined.
|
||||||
|
|
||||||
### Anatomy of a strategy
|
### Anatomy of a strategy
|
||||||
|
|
||||||
A strategy file contains all the information needed to build a good strategy:
|
A strategy file contains all the information needed to build a good strategy:
|
||||||
@ -67,6 +57,46 @@ file as reference.**
|
|||||||
needs to take care to avoid having the strategy utilize data from the future.
|
needs to take care to avoid having the strategy utilize data from the future.
|
||||||
Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document.
|
Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document.
|
||||||
|
|
||||||
|
### Dataframe
|
||||||
|
|
||||||
|
Freqtrade uses [pandas](https://pandas.pydata.org/) to store/provide the candlestick (OHLCV) data.
|
||||||
|
Pandas is a great library developed for processing large amounts of data.
|
||||||
|
|
||||||
|
Each row in a dataframe corresponds to one candle on a chart, with the latest candle always being the last in the dataframe (sorted by date).
|
||||||
|
|
||||||
|
``` output
|
||||||
|
> dataframe.head()
|
||||||
|
date open high low close volume
|
||||||
|
0 2021-11-09 23:25:00+00:00 67279.67 67321.84 67255.01 67300.97 44.62253
|
||||||
|
1 2021-11-09 23:30:00+00:00 67300.97 67301.34 67183.03 67187.01 61.38076
|
||||||
|
2 2021-11-09 23:35:00+00:00 67187.02 67187.02 67031.93 67123.81 113.42728
|
||||||
|
3 2021-11-09 23:40:00+00:00 67123.80 67222.40 67080.33 67160.48 78.96008
|
||||||
|
4 2021-11-09 23:45:00+00:00 67160.48 67160.48 66901.26 66943.37 111.39292
|
||||||
|
```
|
||||||
|
|
||||||
|
Pandas provides fast ways to calculate metrics. To benefit from this speed, it's advised to not use loops, but use vectorized methods instead.
|
||||||
|
|
||||||
|
Vectorized operations perform calculations across the whole range of data and are therefore, compared to looping through each row, a lot faster when calculating indicators.
|
||||||
|
|
||||||
|
As a dataframe is a table, simple python comparisons like the following will not work
|
||||||
|
|
||||||
|
``` python
|
||||||
|
if dataframe['rsi'] > 30:
|
||||||
|
dataframe['buy'] = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
The above section will fail with `The truth value of a Series is ambiguous. [...]`.
|
||||||
|
|
||||||
|
This must instead be written in a pandas-compatible way, so the operation is performed across the whole dataframe.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe['rsi'] > 30)
|
||||||
|
, 'buy'] = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
With this section, you have a new column in your dataframe, which has `1` assigned whenever RSI is above 30.
|
||||||
|
|
||||||
### Customize Indicators
|
### Customize Indicators
|
||||||
|
|
||||||
Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
||||||
@ -134,7 +164,7 @@ Additional technical libraries can be installed as necessary, or custom indicato
|
|||||||
|
|
||||||
### Strategy startup period
|
### Strategy startup period
|
||||||
|
|
||||||
Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
|
Most indicators have an instable startup period, in which they are either not available (NaN), or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
|
||||||
To account for this, the strategy can be assigned the `startup_candle_count` attribute.
|
To account for this, the strategy can be assigned the `startup_candle_count` attribute.
|
||||||
This should be set to the maximum number of candles that the strategy requires to calculate stable indicators.
|
This should be set to the maximum number of candles that the strategy requires to calculate stable indicators.
|
||||||
|
|
||||||
@ -146,8 +176,14 @@ In this example strategy, this should be set to 100 (`startup_candle_count = 100
|
|||||||
|
|
||||||
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
|
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
|
||||||
|
|
||||||
|
!!! Warning "Using x calls to get OHLCV"
|
||||||
|
If you receive a warning like `WARNING - Using 3 calls to get OHLCV. This can result in slower operations for the bot. Please check if you really need 1500 candles for your strategy` - you should consider if you really need this much historic data for your signals.
|
||||||
|
Having this will cause Freqtrade to make multiple calls for the same pair, which will obviously be slower than one network request.
|
||||||
|
As a consequence, Freqtrade will take longer to refresh candles - and should therefore be avoided if possible.
|
||||||
|
This is capped to 5 total calls to avoid overloading the exchange, or make freqtrade too slow.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
`startup_candle_count` should be below `ohlcv_candle_limit` (which is 500 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations.
|
`startup_candle_count` should be below `ohlcv_candle_limit * 5` (which is 500 * 5 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations.
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
@ -281,20 +317,14 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
Setting a stoploss is highly recommended to protect your capital from strong moves against you.
|
Setting a stoploss is highly recommended to protect your capital from strong moves against you.
|
||||||
|
|
||||||
Sample:
|
Sample of setting a 10% stoploss:
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
stoploss = -0.10
|
stoploss = -0.10
|
||||||
```
|
```
|
||||||
|
|
||||||
This would signify a stoploss of -10%.
|
|
||||||
|
|
||||||
For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md).
|
For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md).
|
||||||
|
|
||||||
If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order_types dictionary, so your stoploss is on the exchange and cannot be missed due to network problems, high load or other reasons.
|
|
||||||
|
|
||||||
For more information on order_types please look [here](configuration.md#understand-order_types).
|
|
||||||
|
|
||||||
### Timeframe (formerly ticker interval)
|
### Timeframe (formerly ticker interval)
|
||||||
|
|
||||||
This is the set of candles the bot should download and use for the analysis.
|
This is the set of candles the bot should download and use for the analysis.
|
||||||
@ -310,9 +340,22 @@ The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `p
|
|||||||
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
|
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
|
||||||
|
|
||||||
The Metadata-dict should not be modified and does not persist information across multiple calls.
|
The Metadata-dict should not be modified and does not persist information across multiple calls.
|
||||||
Instead, have a look at the section [Storing information](strategy-advanced.md#Storing-information)
|
Instead, have a look at the [Storing information](strategy-advanced.md#Storing-information) section.
|
||||||
|
|
||||||
## Additional data (informative_pairs)
|
## Strategy file loading
|
||||||
|
|
||||||
|
By default, freqtrade will attempt to load strategies from all `.py` files within `user_data/strategies`.
|
||||||
|
|
||||||
|
Assuming your strategy is called `AwesomeStrategy`, stored in the file `user_data/strategies/AwesomeStrategy.py`, then you can start freqtrade with `freqtrade trade --strategy AwesomeStrategy`.
|
||||||
|
Note that we're using the class-name, not the file name.
|
||||||
|
|
||||||
|
You can use `freqtrade list-strategies` to see a list of all strategies Freqtrade is able to load (all strategies in the correct folder).
|
||||||
|
It will also include a "status" field, highlighting potential problems.
|
||||||
|
|
||||||
|
??? Hint "Customize strategy directory"
|
||||||
|
You can use a different directory by using `--strategy-path user_data/otherPath`. This parameter is available to all commands that require a strategy.
|
||||||
|
|
||||||
|
## Informative Pairs
|
||||||
|
|
||||||
### Get data for non-tradeable pairs
|
### Get data for non-tradeable pairs
|
||||||
|
|
||||||
@ -341,6 +384,133 @@ A full sample can be found [in the DataProvider section](#complete-data-provider
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
### Informative pairs decorator (`@informative()`)
|
||||||
|
|
||||||
|
In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation,
|
||||||
|
not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method.
|
||||||
|
When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter)
|
||||||
|
for more information.
|
||||||
|
|
||||||
|
??? info "Full documentation"
|
||||||
|
``` python
|
||||||
|
def informative(timeframe: str, asset: str = '',
|
||||||
|
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
|
||||||
|
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||||
|
"""
|
||||||
|
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||||
|
define informative indicators.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||||
|
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||||
|
current pair.
|
||||||
|
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||||
|
specified, defaults to:
|
||||||
|
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||||
|
* {column}_{timeframe} if asset is not specified.
|
||||||
|
Format string supports these format variables:
|
||||||
|
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||||
|
* {base} - base currency in lower case, for example 'eth'.
|
||||||
|
* {BASE} - same as {base}, except in upper case.
|
||||||
|
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||||
|
* {QUOTE} - same as {quote}, except in upper case.
|
||||||
|
* {column} - name of dataframe column.
|
||||||
|
* {timeframe} - timeframe of informative dataframe.
|
||||||
|
:param ffill: ffill dataframe after merging informative pair.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
??? Example "Fast and easy way to define informative pairs"
|
||||||
|
|
||||||
|
Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy, informative
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# This method is not required.
|
||||||
|
# def informative_pairs(self): ...
|
||||||
|
|
||||||
|
# Define informative upper timeframe for each pair. Decorators can be stacked on same
|
||||||
|
# method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'.
|
||||||
|
@informative('30m')
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/STAKE informative pair. Available in populate_indicators and other methods as
|
||||||
|
# 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable
|
||||||
|
# instead of hardcoding actual stake currency. Available in populate_indicators and other
|
||||||
|
# methods as 'btc_usdt_rsi_1h' (when stake currency is USDT).
|
||||||
|
@informative('1h', 'BTC/{stake}')
|
||||||
|
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/ETH informative pair. You must specify quote currency if it is different from
|
||||||
|
# stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'.
|
||||||
|
@informative('1h', 'ETH/BTC')
|
||||||
|
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/STAKE informative pair. A custom formatter may be specified for formatting
|
||||||
|
# column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom
|
||||||
|
# formatting. Available in populate_indicators and other methods as 'rsi_upper'.
|
||||||
|
@informative('1h', 'BTC/{stake}', '{column}')
|
||||||
|
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# Strategy timeframe indicators for current pair.
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
# Informative pairs are available in this method.
|
||||||
|
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs
|
||||||
|
manually as described [in the DataProvider section](#complete-data-provider-sample).
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
stake = self.config['stake_currency']
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe[f'btc_{stake}_rsi_1h'] < 35)
|
||||||
|
&
|
||||||
|
(dataframe['volume'] > 0)
|
||||||
|
),
|
||||||
|
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`.
|
||||||
|
|
||||||
|
!!! Warning "Duplicate method names"
|
||||||
|
Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method)
|
||||||
|
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
|
||||||
|
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
|
||||||
|
|
||||||
|
|
||||||
## Additional data (DataProvider)
|
## Additional data (DataProvider)
|
||||||
|
|
||||||
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
|
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
|
||||||
@ -384,9 +554,9 @@ The strategy might look something like this:
|
|||||||
|
|
||||||
*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to buy and sell.*
|
*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to buy and sell.*
|
||||||
|
|
||||||
Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least!
|
Due to the limited available data, it's very difficult to resample `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least!
|
||||||
|
|
||||||
Since we can't resample our data we will have to use an informative pair; and since our whitelist will be dynamic we don't know which pair(s) to use.
|
Since we can't resample the data we will have to use an informative pair; and since the whitelist will be dynamic we don't know which pair(s) to use.
|
||||||
|
|
||||||
This is where calling `self.dp.current_whitelist()` comes in handy.
|
This is where calling `self.dp.current_whitelist()` comes in handy.
|
||||||
|
|
||||||
@ -686,131 +856,6 @@ In some situations it may be confusing to deal with stops relative to current ra
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### *@informative()*
|
|
||||||
|
|
||||||
``` python
|
|
||||||
def informative(timeframe: str, asset: str = '',
|
|
||||||
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
|
|
||||||
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
|
||||||
"""
|
|
||||||
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
|
||||||
define informative indicators.
|
|
||||||
|
|
||||||
Example usage:
|
|
||||||
|
|
||||||
@informative('1h')
|
|
||||||
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
|
||||||
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
|
||||||
current pair.
|
|
||||||
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
|
||||||
specified, defaults to:
|
|
||||||
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
|
||||||
* {column}_{timeframe} if asset is not specified.
|
|
||||||
Format string supports these format variables:
|
|
||||||
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
|
||||||
* {base} - base currency in lower case, for example 'eth'.
|
|
||||||
* {BASE} - same as {base}, except in upper case.
|
|
||||||
* {quote} - quote currency in lower case, for example 'usdt'.
|
|
||||||
* {QUOTE} - same as {quote}, except in upper case.
|
|
||||||
* {column} - name of dataframe column.
|
|
||||||
* {timeframe} - timeframe of informative dataframe.
|
|
||||||
:param ffill: ffill dataframe after merging informative pair.
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation,
|
|
||||||
not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method.
|
|
||||||
When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter)
|
|
||||||
for more information.
|
|
||||||
|
|
||||||
??? Example "Fast and easy way to define informative pairs"
|
|
||||||
|
|
||||||
Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
from freqtrade.strategy import IStrategy, informative
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# This method is not required.
|
|
||||||
# def informative_pairs(self): ...
|
|
||||||
|
|
||||||
# Define informative upper timeframe for each pair. Decorators can be stacked on same
|
|
||||||
# method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'.
|
|
||||||
@informative('30m')
|
|
||||||
@informative('1h')
|
|
||||||
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
# Define BTC/STAKE informative pair. Available in populate_indicators and other methods as
|
|
||||||
# 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable
|
|
||||||
# instead of hardcoding actual stake currency. Available in populate_indicators and other
|
|
||||||
# methods as 'btc_usdt_rsi_1h' (when stake currency is USDT).
|
|
||||||
@informative('1h', 'BTC/{stake}')
|
|
||||||
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
# Define BTC/ETH informative pair. You must specify quote currency if it is different from
|
|
||||||
# stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'.
|
|
||||||
@informative('1h', 'ETH/BTC')
|
|
||||||
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
# Define BTC/STAKE informative pair. A custom formatter may be specified for formatting
|
|
||||||
# column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom
|
|
||||||
# formatting. Available in populate_indicators and other methods as 'rsi_upper'.
|
|
||||||
@informative('1h', 'BTC/{stake}', '{column}')
|
|
||||||
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
# Strategy timeframe indicators for current pair.
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
|
||||||
# Informative pairs are available in this method.
|
|
||||||
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs
|
|
||||||
manually as described [in the DataProvider section](#complete-data-provider-sample).
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
stake = self.config['stake_currency']
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
(dataframe[f'btc_{stake}_rsi_1h'] < 35)
|
|
||||||
&
|
|
||||||
(dataframe['volume'] > 0)
|
|
||||||
),
|
|
||||||
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`.
|
|
||||||
|
|
||||||
!!! Warning "Duplicate method names"
|
|
||||||
Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method)
|
|
||||||
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
|
|
||||||
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
|
|
||||||
|
|
||||||
## Additional data (Wallets)
|
## Additional data (Wallets)
|
||||||
|
|
||||||
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
|
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
|
||||||
@ -894,7 +939,8 @@ Sometimes it may be desired to lock a pair after certain events happen (e.g. mul
|
|||||||
Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until, [reason])`.
|
Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until, [reason])`.
|
||||||
`until` must be a datetime object in the future, after which trading will be re-enabled for that pair, while `reason` is an optional string detailing why the pair was locked.
|
`until` must be a datetime object in the future, after which trading will be re-enabled for that pair, while `reason` is an optional string detailing why the pair was locked.
|
||||||
|
|
||||||
Locks can also be lifted manually, by calling `self.unlock_pair(pair)`.
|
Locks can also be lifted manually, by calling `self.unlock_pair(pair)` or `self.unlock_reason(<reason>)` - providing reason the pair was locked with.
|
||||||
|
`self.unlock_reason(<reason>)` will unlock all pairs currently locked with the provided reason.
|
||||||
|
|
||||||
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
||||||
|
|
||||||
@ -964,9 +1010,13 @@ The following lists some common patterns which should be avoided to prevent frus
|
|||||||
- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling(<window>).mean()` instead
|
- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling(<window>).mean()` instead
|
||||||
- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
|
- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
|
||||||
|
|
||||||
|
### Colliding signals
|
||||||
|
|
||||||
|
When buy and sell signals collide (both `'buy'` and `'sell'` are 1), freqtrade will do nothing and ignore the entry (buy) signal. This will avoid trades that buy, and sell immediately. Obviously, this can potentially lead to missed entries.
|
||||||
|
|
||||||
## Further strategy ideas
|
## Further strategy ideas
|
||||||
|
|
||||||
To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
|
To get additional Ideas for strategies, head over to the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
|
||||||
Feel free to use any of them as inspiration for your own strategies.
|
Feel free to use any of them as inspiration for your own strategies.
|
||||||
We're happy to accept Pull Requests containing new Strategies to that repo.
|
We're happy to accept Pull Requests containing new Strategies to that repo.
|
||||||
|
|
||||||
|
@ -50,7 +50,9 @@ candles.head()
|
|||||||
```python
|
```python
|
||||||
# Load strategy using values set above
|
# Load strategy using values set above
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
strategy = StrategyResolver.load_strategy(config)
|
strategy = StrategyResolver.load_strategy(config)
|
||||||
|
strategy.dp = DataProvider(config, None, None)
|
||||||
|
|
||||||
# Generate buy/sell signals using strategy
|
# Generate buy/sell signals using strategy
|
||||||
df = strategy.analyze_ticker(candles, {'pair': pair})
|
df = strategy.analyze_ticker(candles, {'pair': pair})
|
||||||
@ -228,7 +230,7 @@ graph = generate_candlestick_graph(pair=pair,
|
|||||||
# Show graph inline
|
# Show graph inline
|
||||||
# graph.show()
|
# graph.show()
|
||||||
|
|
||||||
# Render graph in a separate window
|
# Render graph in a seperate window
|
||||||
graph.show(renderer="browser")
|
graph.show(renderer="browser")
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -58,6 +58,8 @@ For the Freqtrade configuration, you can then use the the full value (including
|
|||||||
```json
|
```json
|
||||||
"chat_id": "-1001332619709"
|
"chat_id": "-1001332619709"
|
||||||
```
|
```
|
||||||
|
!!! Warning "Using telegram groups"
|
||||||
|
When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasent surprises.
|
||||||
|
|
||||||
## Control telegram noise
|
## Control telegram noise
|
||||||
|
|
||||||
@ -171,10 +173,12 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
||||||
| `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
|
| `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||||
| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
|
| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
|
||||||
| `/forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
|
| `/forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`forcebuy_enable` must be set to True)
|
||||||
| `/performance` | Show performance of each finished trade grouped by pair
|
| `/performance` | Show performance of each finished trade grouped by pair
|
||||||
| `/balance` | Show account balance per currency
|
| `/balance` | Show account balance per currency
|
||||||
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
||||||
|
| `/weekly <n>` | Shows profit or loss per week, over the last n weeks (n defaults to 8)
|
||||||
|
| `/monthly <n>` | Shows profit or loss per month, over the last n months (n defaults to 6)
|
||||||
| `/stats` | Shows Wins / losses by Sell reason as well as Avg. holding durations for buys and sells
|
| `/stats` | Shows Wins / losses by Sell reason as well as Avg. holding durations for buys and sells
|
||||||
| `/whitelist` | Show the current whitelist
|
| `/whitelist` | Show the current whitelist
|
||||||
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||||
@ -307,8 +311,7 @@ Return the balance of all crypto-currency your have on the exchange.
|
|||||||
|
|
||||||
### /daily <n>
|
### /daily <n>
|
||||||
|
|
||||||
Per default `/daily` will return the 7 last days.
|
Per default `/daily` will return the 7 last days. The example below if for `/daily 3`:
|
||||||
The example below if for `/daily 3`:
|
|
||||||
|
|
||||||
> **Daily Profit over the last 3 days:**
|
> **Daily Profit over the last 3 days:**
|
||||||
```
|
```
|
||||||
@ -319,6 +322,34 @@ Day Profit BTC Profit USD
|
|||||||
2018-01-01 0.00269130 BTC 34.986 USD
|
2018-01-01 0.00269130 BTC 34.986 USD
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### /weekly <n>
|
||||||
|
|
||||||
|
Per default `/weekly` will return the 8 last weeks, including the current week. Each week starts
|
||||||
|
from Monday. The example below if for `/weekly 3`:
|
||||||
|
|
||||||
|
> **Weekly Profit over the last 3 weeks (starting from Monday):**
|
||||||
|
```
|
||||||
|
Monday Profit BTC Profit USD
|
||||||
|
---------- -------------- ------------
|
||||||
|
2018-01-03 0.00224175 BTC 29,142 USD
|
||||||
|
2017-12-27 0.00033131 BTC 4,307 USD
|
||||||
|
2017-12-20 0.00269130 BTC 34.986 USD
|
||||||
|
```
|
||||||
|
|
||||||
|
### /monthly <n>
|
||||||
|
|
||||||
|
Per default `/monthly` will return the 6 last months, including the current month. The example below
|
||||||
|
if for `/monthly 3`:
|
||||||
|
|
||||||
|
> **Monthly Profit over the last 3 months:**
|
||||||
|
```
|
||||||
|
Month Profit BTC Profit USD
|
||||||
|
---------- -------------- ------------
|
||||||
|
2018-01 0.00224175 BTC 29,142 USD
|
||||||
|
2017-12 0.00033131 BTC 4,307 USD
|
||||||
|
2017-11 0.00269130 BTC 34.986 USD
|
||||||
|
```
|
||||||
|
|
||||||
### /whitelist
|
### /whitelist
|
||||||
|
|
||||||
Shows the current whitelist
|
Shows the current whitelist
|
||||||
|
@ -281,7 +281,7 @@ bitmax True missing opt: fetchMyTrades
|
|||||||
bitmex False Various reasons.
|
bitmex False Various reasons.
|
||||||
bitpanda True
|
bitpanda True
|
||||||
bitso False missing: fetchOHLCV
|
bitso False missing: fetchOHLCV
|
||||||
bitstamp False Does not provide history. Details in https://github.com/freqtrade/freqtrade/issues/1983
|
bitstamp True missing opt: fetchTickers
|
||||||
bitstamp1 False missing: fetchOrder, fetchOHLCV
|
bitstamp1 False missing: fetchOrder, fetchOHLCV
|
||||||
bittrex True
|
bittrex True
|
||||||
bitvavo True
|
bitvavo True
|
||||||
@ -577,6 +577,46 @@ Common arguments:
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Show previous Backtest results
|
||||||
|
|
||||||
|
Allows you to show previous backtest results.
|
||||||
|
Adding `--show-pair-list` outputs a sorted pair list you can easily copy/paste into your configuration (omitting bad pairs).
|
||||||
|
|
||||||
|
??? Warning "Strategy overfitting"
|
||||||
|
Only using winning pairs can lead to an overfitted strategy, which will not work well on future data. Make sure to extensively test your strategy in dry-run before risking real money.
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: freqtrade backtesting-show [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
|
[-d PATH] [--userdir PATH]
|
||||||
|
[--export-filename PATH] [--show-pair-list]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--export-filename PATH
|
||||||
|
Save backtest results to the file with this filename.
|
||||||
|
Requires `--export` to be set as well. Example:
|
||||||
|
`--export-filename=user_data/backtest_results/backtest
|
||||||
|
_today.json`
|
||||||
|
--show-pair-list Show backtesting pairlist sorted by profit.
|
||||||
|
|
||||||
|
Common arguments:
|
||||||
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
--logfile FILE Log to the file specified. Special values are:
|
||||||
|
'syslog', 'journald'. See the documentation for more
|
||||||
|
details.
|
||||||
|
-V, --version show program's version number and exit
|
||||||
|
-c PATH, --config PATH
|
||||||
|
Specify configuration file (default:
|
||||||
|
`userdir/config.json` or `config.json` whichever
|
||||||
|
exists). Multiple --config options may be used. Can be
|
||||||
|
set to `-` to read config from stdin.
|
||||||
|
-d PATH, --datadir PATH
|
||||||
|
Path to directory with historical backtesting data.
|
||||||
|
--userdir PATH, --user-data-dir PATH
|
||||||
|
Path to userdata directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## List Hyperopt results
|
## List Hyperopt results
|
||||||
|
|
||||||
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command.
|
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command.
|
||||||
@ -667,6 +707,7 @@ usage: freqtrade hyperopt-show [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[--profitable] [-n INT] [--print-json]
|
[--profitable] [-n INT] [--print-json]
|
||||||
[--hyperopt-filename FILENAME] [--no-header]
|
[--hyperopt-filename FILENAME] [--no-header]
|
||||||
[--disable-param-export]
|
[--disable-param-export]
|
||||||
|
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
@ -680,6 +721,8 @@ optional arguments:
|
|||||||
--no-header Do not print epoch details header.
|
--no-header Do not print epoch details header.
|
||||||
--disable-param-export
|
--disable-param-export
|
||||||
Disable automatic hyperopt parameter export.
|
Disable automatic hyperopt parameter export.
|
||||||
|
--breakdown {day,week,month} [{day,week,month} ...]
|
||||||
|
Show backtesting breakdown per [day, week, month].
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
@ -48,9 +48,9 @@ Sample configuration (tested using IFTTT).
|
|||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert our event and key to the url.
|
The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert your event and key to the url.
|
||||||
|
|
||||||
You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use `"format": "form"` or `"format": "json"` respectively. Example configuration for Mattermost Cloud integration:
|
You can set the POST body format to Form-Encoded (default), JSON-Encoded, or raw data. Use `"format": "form"`, `"format": "json"`, or `"format": "raw"` respectively. Example configuration for Mattermost Cloud integration:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"webhook": {
|
"webhook": {
|
||||||
@ -63,7 +63,36 @@ You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use
|
|||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
The result would be POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel.
|
The result would be a POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel.
|
||||||
|
|
||||||
|
When using the Form-Encoded or JSON-Encoded configuration you can configure any number of payload values, and both the key and value will be ouput in the POST request. However, when using the raw data format you can only configure one value and it **must** be named `"data"`. In this instance the data key will not be output in the POST request, only the value. For example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"webhook": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "https://<YOURHOOKURL>",
|
||||||
|
"format": "raw",
|
||||||
|
"webhookstatus": {
|
||||||
|
"data": "Status: {status}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
The result would be a POST request with e.g. `Status: running` body and `Content-Type: text/plain` header.
|
||||||
|
|
||||||
|
Optional parameters are available to enable automatic retries for webhook messages. The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook. Example configuration for retries:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"webhook": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "https://<YOURHOOKURL>",
|
||||||
|
"retries": 3,
|
||||||
|
"retry_delay": 0.2,
|
||||||
|
"webhookstatus": {
|
||||||
|
"status": "Status: {status}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
|
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
|
||||||
|
|
||||||
@ -75,7 +104,8 @@ Possible parameters are:
|
|||||||
* `trade_id`
|
* `trade_id`
|
||||||
* `exchange`
|
* `exchange`
|
||||||
* `pair`
|
* `pair`
|
||||||
* `limit`
|
* ~~`limit` # Deprecated - should no longer be used.~~
|
||||||
|
* `open_rate`
|
||||||
* `amount`
|
* `amount`
|
||||||
* `open_date`
|
* `open_date`
|
||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
@ -117,6 +147,8 @@ Possible parameters are:
|
|||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
|
* `order_type`
|
||||||
|
* `current_rate`
|
||||||
* `buy_tag`
|
* `buy_tag`
|
||||||
|
|
||||||
### Webhooksell
|
### Webhooksell
|
||||||
|
@ -16,7 +16,8 @@ from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hype
|
|||||||
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets,
|
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets,
|
||||||
start_list_strategies, start_list_timeframes,
|
start_list_strategies, start_list_timeframes,
|
||||||
start_show_trades)
|
start_show_trades)
|
||||||
from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt
|
from freqtrade.commands.optimize_commands import (start_backtesting, start_backtesting_show,
|
||||||
|
start_edge, start_hyperopt)
|
||||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||||
from freqtrade.commands.trade_commands import start_trading
|
from freqtrade.commands.trade_commands import start_trading
|
||||||
|
@ -23,7 +23,8 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
|
|||||||
|
|
||||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
||||||
"enable_protections", "dry_run_wallet", "timeframe_detail",
|
"enable_protections", "dry_run_wallet", "timeframe_detail",
|
||||||
"strategy_list", "export", "exportfilename"]
|
"strategy_list", "export", "exportfilename",
|
||||||
|
"backtest_breakdown"]
|
||||||
|
|
||||||
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||||
"position_stacking", "use_max_market_positions",
|
"position_stacking", "use_max_market_positions",
|
||||||
@ -40,6 +41,8 @@ ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized"]
|
|||||||
|
|
||||||
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
|
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
|
||||||
|
|
||||||
|
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list"]
|
||||||
|
|
||||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
||||||
|
|
||||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
||||||
@ -89,11 +92,11 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
|
|||||||
|
|
||||||
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
|
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
|
||||||
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
|
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
|
||||||
"disableparamexport"]
|
"disableparamexport", "backtest_breakdown"]
|
||||||
|
|
||||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||||
"hyperopt-list", "hyperopt-show",
|
"hyperopt-list", "hyperopt-show", "backtest-filter",
|
||||||
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
|
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
|
||||||
|
|
||||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||||
@ -172,7 +175,8 @@ class Arguments:
|
|||||||
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
||||||
self._build_args(optionlist=['version'], parser=self.parser)
|
self._build_args(optionlist=['version'], parser=self.parser)
|
||||||
|
|
||||||
from freqtrade.commands import (start_backtesting, start_convert_data, start_convert_trades,
|
from freqtrade.commands import (start_backtesting, start_backtesting_show,
|
||||||
|
start_convert_data, start_convert_trades,
|
||||||
start_create_userdir, start_download_data, start_edge,
|
start_create_userdir, start_download_data, start_edge,
|
||||||
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
|
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
|
||||||
start_install_ui, start_list_data, start_list_exchanges,
|
start_install_ui, start_list_data, start_list_exchanges,
|
||||||
@ -263,6 +267,15 @@ class Arguments:
|
|||||||
backtesting_cmd.set_defaults(func=start_backtesting)
|
backtesting_cmd.set_defaults(func=start_backtesting)
|
||||||
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
|
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
|
||||||
|
|
||||||
|
# Add backtesting-show subcommand
|
||||||
|
backtesting_show_cmd = subparsers.add_parser(
|
||||||
|
'backtesting-show',
|
||||||
|
help='Show past Backtest results',
|
||||||
|
parents=[_common_parser],
|
||||||
|
)
|
||||||
|
backtesting_show_cmd.set_defaults(func=start_backtesting_show)
|
||||||
|
self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtesting_show_cmd)
|
||||||
|
|
||||||
# Add edge subcommand
|
# Add edge subcommand
|
||||||
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
|
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
|
||||||
parents=[_common_parser, _strategy_parser])
|
parents=[_common_parser, _strategy_parser])
|
||||||
|
@ -83,11 +83,19 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
if val == UNLIMITED_STAKE_AMOUNT
|
if val == UNLIMITED_STAKE_AMOUNT
|
||||||
else val
|
else val
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"name": "timeframe_in_config",
|
||||||
|
"message": "Tim",
|
||||||
|
"choices": ["Have the strategy define timeframe.", "Override in configuration."]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "timeframe",
|
"name": "timeframe",
|
||||||
"message": "Please insert your desired timeframe (e.g. 5m):",
|
"message": "Please insert your desired timeframe (e.g. 5m):",
|
||||||
"default": "5m",
|
"default": "5m",
|
||||||
|
"when": lambda x: x["timeframe_in_config"] == 'Override in configuration.'
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@ -107,6 +115,7 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"ftx",
|
"ftx",
|
||||||
"kucoin",
|
"kucoin",
|
||||||
"gateio",
|
"gateio",
|
||||||
|
"okex",
|
||||||
Separator(),
|
Separator(),
|
||||||
"other",
|
"other",
|
||||||
],
|
],
|
||||||
@ -134,7 +143,7 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"type": "password",
|
"type": "password",
|
||||||
"name": "exchange_key_password",
|
"name": "exchange_key_password",
|
||||||
"message": "Insert Exchange API Key password",
|
"message": "Insert Exchange API Key password",
|
||||||
"when": lambda x: not x['dry_run'] and x['exchange_name'] == 'kucoin'
|
"when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okex')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "confirm",
|
"type": "confirm",
|
||||||
|
@ -152,6 +152,12 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
action='store_false',
|
action='store_false',
|
||||||
default=True,
|
default=True,
|
||||||
),
|
),
|
||||||
|
"backtest_show_pair_list": Arg(
|
||||||
|
'--show-pair-list',
|
||||||
|
help='Show backtesting pairlist sorted by profit.',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
"enable_protections": Arg(
|
"enable_protections": Arg(
|
||||||
'--enable-protections', '--enableprotections',
|
'--enable-protections', '--enableprotections',
|
||||||
help='Enable protections for backtesting.'
|
help='Enable protections for backtesting.'
|
||||||
@ -193,6 +199,12 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
type=float,
|
type=float,
|
||||||
metavar='FLOAT',
|
metavar='FLOAT',
|
||||||
),
|
),
|
||||||
|
"backtest_breakdown": Arg(
|
||||||
|
'--breakdown',
|
||||||
|
help='Show backtesting breakdown per [day, week, month].',
|
||||||
|
nargs='+',
|
||||||
|
choices=constants.BACKTEST_BREAKDOWNS
|
||||||
|
),
|
||||||
# Edge
|
# Edge
|
||||||
"stoploss_range": Arg(
|
"stoploss_range": Arg(
|
||||||
'--stoplosses',
|
'--stoplosses',
|
||||||
|
@ -96,7 +96,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
|||||||
if 'strategy_name' in metrics:
|
if 'strategy_name' in metrics:
|
||||||
strategy_name = metrics['strategy_name']
|
strategy_name = metrics['strategy_name']
|
||||||
show_backtest_result(strategy_name, metrics,
|
show_backtest_result(strategy_name, metrics,
|
||||||
metrics['stake_currency'])
|
metrics['stake_currency'], config.get('backtest_breakdown', []))
|
||||||
|
|
||||||
HyperoptTools.try_export_params(config, strategy_name, val)
|
HyperoptTools.try_export_params(config, strategy_name, val)
|
||||||
|
|
||||||
|
@ -54,6 +54,22 @@ def start_backtesting(args: Dict[str, Any]) -> None:
|
|||||||
backtesting.start()
|
backtesting.start()
|
||||||
|
|
||||||
|
|
||||||
|
def start_backtesting_show(args: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Show previous backtest result
|
||||||
|
"""
|
||||||
|
|
||||||
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
|
from freqtrade.data.btanalysis import load_backtest_stats
|
||||||
|
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
|
||||||
|
|
||||||
|
results = load_backtest_stats(config['exportfilename'])
|
||||||
|
|
||||||
|
show_backtest_results(config, results)
|
||||||
|
show_sorted_pairlist(config, results)
|
||||||
|
|
||||||
|
|
||||||
def start_hyperopt(args: Dict[str, Any]) -> None:
|
def start_hyperopt(args: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Start hyperopt script
|
Start hyperopt script
|
||||||
|
@ -245,6 +245,10 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='timeframe_detail',
|
self._args_to_config(config, argname='timeframe_detail',
|
||||||
logstring='Parameter --timeframe-detail detected, '
|
logstring='Parameter --timeframe-detail detected, '
|
||||||
'using {} for intra-candle backtesting ...')
|
'using {} for intra-candle backtesting ...')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='backtest_show_pair_list',
|
||||||
|
logstring='Parameter --show-pair-list detected.')
|
||||||
|
|
||||||
self._args_to_config(config, argname='stake_amount',
|
self._args_to_config(config, argname='stake_amount',
|
||||||
logstring='Parameter --stake-amount detected, '
|
logstring='Parameter --stake-amount detected, '
|
||||||
'overriding stake_amount to: {} ...')
|
'overriding stake_amount to: {} ...')
|
||||||
@ -269,8 +273,12 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='export',
|
self._args_to_config(config, argname='export',
|
||||||
logstring='Parameter --export detected: {} ...')
|
logstring='Parameter --export detected: {} ...')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='backtest_breakdown',
|
||||||
|
logstring='Parameter --breakdown detected ...')
|
||||||
|
|
||||||
self._args_to_config(config, argname='disableparamexport',
|
self._args_to_config(config, argname='disableparamexport',
|
||||||
logstring='Parameter --disableparamexport detected: {} ...')
|
logstring='Parameter --disableparamexport detected: {} ...')
|
||||||
|
|
||||||
# Edge section:
|
# Edge section:
|
||||||
if 'stoploss_range' in self.args and self.args["stoploss_range"]:
|
if 'stoploss_range' in self.args and self.args["stoploss_range"]:
|
||||||
txt_range = eval(self.args["stoploss_range"])
|
txt_range = eval(self.args["stoploss_range"])
|
||||||
|
@ -32,6 +32,7 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
|
|||||||
:param prefix: Prefix to consider (usually FREQTRADE__)
|
:param prefix: Prefix to consider (usually FREQTRADE__)
|
||||||
:return: Nested dict based on available and relevant variables.
|
:return: Nested dict based on available and relevant variables.
|
||||||
"""
|
"""
|
||||||
|
no_convert = ['CHAT_ID']
|
||||||
relevant_vars: Dict[str, Any] = {}
|
relevant_vars: Dict[str, Any] = {}
|
||||||
|
|
||||||
for env_var, val in sorted(env_dict.items()):
|
for env_var, val in sorted(env_dict.items()):
|
||||||
@ -39,9 +40,9 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
|
|||||||
logger.info(f"Loading variable '{env_var}'")
|
logger.info(f"Loading variable '{env_var}'")
|
||||||
key = env_var.replace(prefix, '')
|
key = env_var.replace(prefix, '')
|
||||||
for k in reversed(key.split('__')):
|
for k in reversed(key.split('__')):
|
||||||
val = {k.lower(): get_var_typed(val) if type(val) != dict else val}
|
val = {k.lower(): get_var_typed(val)
|
||||||
|
if type(val) != dict and k not in no_convert else val}
|
||||||
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
||||||
|
|
||||||
return relevant_vars
|
return relevant_vars
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
|||||||
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||||
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
||||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
||||||
|
'CalmarHyperOptLoss',
|
||||||
'MaxDrawDownHyperOptLoss']
|
'MaxDrawDownHyperOptLoss']
|
||||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||||
@ -32,6 +33,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
|||||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||||
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
|
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
|
||||||
|
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
||||||
DRY_RUN_WALLET = 1000
|
DRY_RUN_WALLET = 1000
|
||||||
DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S'
|
DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||||
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
||||||
@ -48,11 +50,12 @@ USERPATH_STRATEGIES = 'strategies'
|
|||||||
USERPATH_NOTEBOOKS = 'notebooks'
|
USERPATH_NOTEBOOKS = 'notebooks'
|
||||||
|
|
||||||
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
||||||
|
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
||||||
|
|
||||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||||
|
|
||||||
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
|
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
|
||||||
|
|
||||||
|
|
||||||
# Define decimals per coin for outputs
|
# Define decimals per coin for outputs
|
||||||
# Only used for outputs.
|
# Only used for outputs.
|
||||||
DECIMAL_PER_COIN_FALLBACK = 3 # Should be low to avoid listing all possible FIAT's
|
DECIMAL_PER_COIN_FALLBACK = 3 # Should be low to avoid listing all possible FIAT's
|
||||||
@ -66,7 +69,6 @@ DUST_PER_COIN = {
|
|||||||
'ETH': 0.01
|
'ETH': 0.01
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Source files with destination directories within user-directory
|
# Source files with destination directories within user-directory
|
||||||
USER_DATA_FILES = {
|
USER_DATA_FILES = {
|
||||||
'sample_strategy.py': USERPATH_STRATEGIES,
|
'sample_strategy.py': USERPATH_STRATEGIES,
|
||||||
@ -146,12 +148,17 @@ CONF_SCHEMA = {
|
|||||||
'sell_profit_offset': {'type': 'number'},
|
'sell_profit_offset': {'type': 'number'},
|
||||||
'ignore_roi_if_buy_signal': {'type': 'boolean'},
|
'ignore_roi_if_buy_signal': {'type': 'boolean'},
|
||||||
'ignore_buying_expired_candle_after': {'type': 'number'},
|
'ignore_buying_expired_candle_after': {'type': 'number'},
|
||||||
|
'backtest_breakdown': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS}
|
||||||
|
},
|
||||||
'bot_name': {'type': 'string'},
|
'bot_name': {'type': 'string'},
|
||||||
'unfilledtimeout': {
|
'unfilledtimeout': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'buy': {'type': 'number', 'minimum': 1},
|
'buy': {'type': 'number', 'minimum': 1},
|
||||||
'sell': {'type': 'number', 'minimum': 1},
|
'sell': {'type': 'number', 'minimum': 1},
|
||||||
|
'exit_timeout_count': {'type': 'number', 'minimum': 0, 'default': 0},
|
||||||
'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'}
|
'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -193,7 +200,7 @@ CONF_SCHEMA = {
|
|||||||
'required': ['price_side']
|
'required': ['price_side']
|
||||||
},
|
},
|
||||||
'custom_price_max_distance_ratio': {
|
'custom_price_max_distance_ratio': {
|
||||||
'type': 'number', 'minimum': 0.0
|
'type': 'number', 'minimum': 0.0
|
||||||
},
|
},
|
||||||
'order_types': {
|
'order_types': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@ -202,7 +209,10 @@ CONF_SCHEMA = {
|
|||||||
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'forcesell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'forcesell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'forcebuy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'forcebuy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'emergencysell': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': ORDERTYPE_POSSIBILITIES,
|
||||||
|
'default': 'market'},
|
||||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'stoploss_on_exchange': {'type': 'boolean'},
|
'stoploss_on_exchange': {'type': 'boolean'},
|
||||||
'stoploss_on_exchange_interval': {'type': 'number'},
|
'stoploss_on_exchange_interval': {'type': 'number'},
|
||||||
@ -304,10 +314,16 @@ CONF_SCHEMA = {
|
|||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'enabled': {'type': 'boolean'},
|
'enabled': {'type': 'boolean'},
|
||||||
|
'url': {'type': 'string'},
|
||||||
|
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
|
||||||
|
'retries': {'type': 'integer', 'minimum': 0},
|
||||||
|
'retry_delay': {'type': 'number', 'minimum': 0},
|
||||||
'webhookbuy': {'type': 'object'},
|
'webhookbuy': {'type': 'object'},
|
||||||
'webhookbuycancel': {'type': 'object'},
|
'webhookbuycancel': {'type': 'object'},
|
||||||
|
'webhookbuyfill': {'type': 'object'},
|
||||||
'webhooksell': {'type': 'object'},
|
'webhooksell': {'type': 'object'},
|
||||||
'webhooksellcancel': {'type': 'object'},
|
'webhooksellcancel': {'type': 'object'},
|
||||||
|
'webhooksellfill': {'type': 'object'},
|
||||||
'webhookstatus': {'type': 'object'},
|
'webhookstatus': {'type': 'object'},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -346,13 +362,13 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'dataformat_ohlcv': {
|
'dataformat_ohlcv': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'enum': AVAILABLE_DATAHANDLERS,
|
'enum': AVAILABLE_DATAHANDLERS,
|
||||||
'default': 'json'
|
'default': 'json'
|
||||||
},
|
},
|
||||||
'dataformat_trades': {
|
'dataformat_trades': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'enum': AVAILABLE_DATAHANDLERS,
|
'enum': AVAILABLE_DATAHANDLERS,
|
||||||
'default': 'jsongz'
|
'default': 'jsongz'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'definitions': {
|
'definitions': {
|
||||||
|
@ -113,7 +113,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
|||||||
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
|
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
|
||||||
if len_before != len_after:
|
if len_before != len_after:
|
||||||
message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}"
|
message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}"
|
||||||
f" - {round(pct_missing * 100, 2)}%")
|
f" - {pct_missing:.2%}")
|
||||||
if pct_missing > 0.01:
|
if pct_missing > 0.01:
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
else:
|
else:
|
||||||
|
@ -6,7 +6,6 @@ from typing import List, Optional
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade import misc
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS,
|
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS,
|
||||||
ListPairsWithTimeframes, TradeList)
|
ListPairsWithTimeframes, TradeList)
|
||||||
@ -61,10 +60,10 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
|
|
||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
||||||
|
|
||||||
ds = pd.HDFStore(filename, mode='a', complevel=9, complib='blosc')
|
_data.loc[:, self._columns].to_hdf(
|
||||||
ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date'])
|
filename, key, mode='a', complevel=9, complib='blosc',
|
||||||
|
format='table', data_columns=['date']
|
||||||
ds.close()
|
)
|
||||||
|
|
||||||
def _ohlcv_load(self, pair: str, timeframe: str,
|
def _ohlcv_load(self, pair: str, timeframe: str,
|
||||||
timerange: Optional[TimeRange] = None) -> pd.DataFrame:
|
timerange: Optional[TimeRange] = None) -> pd.DataFrame:
|
||||||
@ -99,19 +98,6 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
|
|
||||||
"""
|
|
||||||
Remove data for this pair
|
|
||||||
:param pair: Delete data for this pair.
|
|
||||||
:param timeframe: Timeframe (e.g. "5m")
|
|
||||||
:return: True when deleted, false if file did not exist.
|
|
||||||
"""
|
|
||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
|
||||||
if filename.exists():
|
|
||||||
filename.unlink()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def ohlcv_append(self, pair: str, timeframe: str, data: pd.DataFrame) -> None:
|
def ohlcv_append(self, pair: str, timeframe: str, data: pd.DataFrame) -> None:
|
||||||
"""
|
"""
|
||||||
Append data to existing data structures
|
Append data to existing data structures
|
||||||
@ -142,11 +128,11 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
"""
|
"""
|
||||||
key = self._pair_trades_key(pair)
|
key = self._pair_trades_key(pair)
|
||||||
|
|
||||||
ds = pd.HDFStore(self._pair_trades_filename(self._datadir, pair),
|
pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS).to_hdf(
|
||||||
mode='a', complevel=9, complib='blosc')
|
self._pair_trades_filename(self._datadir, pair), key,
|
||||||
ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS),
|
mode='a', complevel=9, complib='blosc',
|
||||||
format='table', data_columns=['timestamp'])
|
format='table', data_columns=['timestamp']
|
||||||
ds.close()
|
)
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
def trades_append(self, pair: str, data: TradeList):
|
||||||
"""
|
"""
|
||||||
@ -180,17 +166,9 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
||||||
return trades.values.tolist()
|
return trades.values.tolist()
|
||||||
|
|
||||||
def trades_purge(self, pair: str) -> bool:
|
@classmethod
|
||||||
"""
|
def _get_file_extension(cls):
|
||||||
Remove data for this pair
|
return "h5"
|
||||||
:param pair: Delete data for this pair.
|
|
||||||
:return: True when deleted, false if file did not exist.
|
|
||||||
"""
|
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
|
||||||
if filename.exists():
|
|
||||||
filename.unlink()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str:
|
def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str:
|
||||||
@ -199,15 +177,3 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _pair_trades_key(cls, pair: str) -> str:
|
def _pair_trades_key(cls, pair: str) -> str:
|
||||||
return f"{pair}/trades"
|
return f"{pair}/trades"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
|
|
||||||
pair_s = misc.pair_to_filename(pair)
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-{timeframe}.h5')
|
|
||||||
return filename
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
|
|
||||||
pair_s = misc.pair_to_filename(pair)
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-trades.h5')
|
|
||||||
return filename
|
|
||||||
|
@ -12,6 +12,7 @@ from typing import List, Optional, Type
|
|||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import misc
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import ListPairsWithTimeframes, TradeList
|
from freqtrade.constants import ListPairsWithTimeframes, TradeList
|
||||||
from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
|
from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
|
||||||
@ -26,6 +27,13 @@ class IDataHandler(ABC):
|
|||||||
def __init__(self, datadir: Path) -> None:
|
def __init__(self, datadir: Path) -> None:
|
||||||
self._datadir = datadir
|
self._datadir = datadir
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_file_extension(cls) -> str:
|
||||||
|
"""
|
||||||
|
Get file extension for this particular datahandler
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@abstractclassmethod
|
@abstractclassmethod
|
||||||
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
|
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
|
||||||
"""
|
"""
|
||||||
@ -70,7 +78,6 @@ class IDataHandler(ABC):
|
|||||||
:return: DataFrame with ohlcv data, or empty DataFrame
|
:return: DataFrame with ohlcv data, or empty DataFrame
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
|
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Remove data for this pair
|
Remove data for this pair
|
||||||
@ -78,6 +85,11 @@ class IDataHandler(ABC):
|
|||||||
:param timeframe: Timeframe (e.g. "5m")
|
:param timeframe: Timeframe (e.g. "5m")
|
||||||
:return: True when deleted, false if file did not exist.
|
:return: True when deleted, false if file did not exist.
|
||||||
"""
|
"""
|
||||||
|
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
||||||
|
if filename.exists():
|
||||||
|
filename.unlink()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def ohlcv_append(self, pair: str, timeframe: str, data: DataFrame) -> None:
|
def ohlcv_append(self, pair: str, timeframe: str, data: DataFrame) -> None:
|
||||||
@ -123,13 +135,17 @@ class IDataHandler(ABC):
|
|||||||
:return: List of trades
|
:return: List of trades
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def trades_purge(self, pair: str) -> bool:
|
def trades_purge(self, pair: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Remove data for this pair
|
Remove data for this pair
|
||||||
:param pair: Delete data for this pair.
|
:param pair: Delete data for this pair.
|
||||||
:return: True when deleted, false if file did not exist.
|
:return: True when deleted, false if file did not exist.
|
||||||
"""
|
"""
|
||||||
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
|
if filename.exists():
|
||||||
|
filename.unlink()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
||||||
"""
|
"""
|
||||||
@ -141,6 +157,18 @@ class IDataHandler(ABC):
|
|||||||
"""
|
"""
|
||||||
return trades_remove_duplicates(self._trades_load(pair, timerange=timerange))
|
return trades_remove_duplicates(self._trades_load(pair, timerange=timerange))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
|
||||||
|
pair_s = misc.pair_to_filename(pair)
|
||||||
|
filename = datadir.joinpath(f'{pair_s}-{timeframe}.{cls._get_file_extension()}')
|
||||||
|
return filename
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
|
||||||
|
pair_s = misc.pair_to_filename(pair)
|
||||||
|
filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}')
|
||||||
|
return filename
|
||||||
|
|
||||||
def ohlcv_load(self, pair, timeframe: str,
|
def ohlcv_load(self, pair, timeframe: str,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange] = None,
|
||||||
fill_missing: bool = True,
|
fill_missing: bool = True,
|
||||||
|
@ -174,34 +174,10 @@ class JsonDataHandler(IDataHandler):
|
|||||||
pass
|
pass
|
||||||
return tradesdata
|
return tradesdata
|
||||||
|
|
||||||
def trades_purge(self, pair: str) -> bool:
|
|
||||||
"""
|
|
||||||
Remove data for this pair
|
|
||||||
:param pair: Delete data for this pair.
|
|
||||||
:return: True when deleted, false if file did not exist.
|
|
||||||
"""
|
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
|
||||||
if filename.exists():
|
|
||||||
filename.unlink()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
|
|
||||||
pair_s = misc.pair_to_filename(pair)
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-{timeframe}.{cls._get_file_extension()}')
|
|
||||||
return filename
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
return "json.gz" if cls._use_zip else "json"
|
return "json.gz" if cls._use_zip else "json"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
|
|
||||||
pair_s = misc.pair_to_filename(pair)
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}')
|
|
||||||
return filename
|
|
||||||
|
|
||||||
|
|
||||||
class JsonGzDataHandler(JsonDataHandler):
|
class JsonGzDataHandler(JsonDataHandler):
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
from freqtrade.enums.backteststate import BacktestState
|
from freqtrade.enums.backteststate import BacktestState
|
||||||
|
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
||||||
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
||||||
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
||||||
from freqtrade.enums.selltype import SellType
|
from freqtrade.enums.selltype import SellType
|
||||||
|
6
freqtrade/enums/ordertypevalue.py
Normal file
6
freqtrade/enums/ordertypevalue.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class OrderTypeValues(str, Enum):
|
||||||
|
limit = 'limit'
|
||||||
|
market = 'market'
|
@ -14,3 +14,4 @@ class SignalTagType(Enum):
|
|||||||
Enum for signal columns
|
Enum for signal columns
|
||||||
"""
|
"""
|
||||||
BUY_TAG = "buy_tag"
|
BUY_TAG = "buy_tag"
|
||||||
|
EXIT_TAG = "exit_tag"
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
class FreqtradeException(Exception):
|
class FreqtradeException(Exception):
|
||||||
"""
|
"""
|
||||||
Freqtrade base exception. Handled at the outermost level.
|
Freqtrade base exception. Handled at the outermost level.
|
||||||
|
@ -19,3 +19,4 @@ from freqtrade.exchange.gateio import Gateio
|
|||||||
from freqtrade.exchange.hitbtc import Hitbtc
|
from freqtrade.exchange.hitbtc import Hitbtc
|
||||||
from freqtrade.exchange.kraken import Kraken
|
from freqtrade.exchange.kraken import Kraken
|
||||||
from freqtrade.exchange.kucoin import Kucoin
|
from freqtrade.exchange.kucoin import Kucoin
|
||||||
|
from freqtrade.exchange.okex import Okex
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
""" Binance exchange subclass """
|
""" Binance exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
@ -93,8 +93,9 @@ class Binance(Exchange):
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
since_ms: int, is_new_pair: bool
|
since_ms: int, is_new_pair: bool = False,
|
||||||
) -> List:
|
raise_: bool = False
|
||||||
|
) -> Tuple[str, str, List]:
|
||||||
"""
|
"""
|
||||||
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
|
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
|
||||||
Does not work for other exchanges, which don't return the earliest data when called with "0"
|
Does not work for other exchanges, which don't return the earliest data when called with "0"
|
||||||
@ -107,4 +108,5 @@ class Binance(Exchange):
|
|||||||
logger.info(f"Candle-data for {pair} available starting with "
|
logger.info(f"Candle-data for {pair} available starting with "
|
||||||
f"{arrow.get(since_ms // 1000).isoformat()}.")
|
f"{arrow.get(since_ms // 1000).isoformat()}.")
|
||||||
return await super()._async_get_historic_ohlcv(
|
return await super()._async_get_historic_ohlcv(
|
||||||
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair)
|
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair,
|
||||||
|
raise_=raise_)
|
||||||
|
@ -16,8 +16,6 @@ API_FETCH_ORDER_RETRY_COUNT = 5
|
|||||||
|
|
||||||
BAD_EXCHANGES = {
|
BAD_EXCHANGES = {
|
||||||
"bitmex": "Various reasons.",
|
"bitmex": "Various reasons.",
|
||||||
"bitstamp": "Does not provide history. "
|
|
||||||
"Details in https://github.com/freqtrade/freqtrade/issues/1983",
|
|
||||||
"phemex": "Does not provide history. ",
|
"phemex": "Does not provide history. ",
|
||||||
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
|
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
|
||||||
}
|
}
|
||||||
@ -83,9 +81,16 @@ def retrier_async(f):
|
|||||||
count -= 1
|
count -= 1
|
||||||
kwargs.update({'count': count})
|
kwargs.update({'count': count})
|
||||||
if isinstance(ex, DDosProtection):
|
if isinstance(ex, DDosProtection):
|
||||||
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
|
if "kucoin" in str(ex) and "429000" in str(ex):
|
||||||
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
# Temporary fix for 429000 error on kucoin
|
||||||
await asyncio.sleep(backoff_delay)
|
# see https://github.com/freqtrade/freqtrade/issues/5700 for details.
|
||||||
|
logger.warning(
|
||||||
|
f"Kucoin 429 error, avoid triggering DDosProtection backoff delay. "
|
||||||
|
f"{count} tries left before giving up")
|
||||||
|
else:
|
||||||
|
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
|
||||||
|
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||||
|
await asyncio.sleep(backoff_delay)
|
||||||
return await wrapper(*args, **kwargs)
|
return await wrapper(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
logger.warning('Giving up retrying: %s()', f.__name__)
|
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||||
|
@ -7,7 +7,7 @@ import http
|
|||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
@ -155,8 +155,8 @@ class Exchange:
|
|||||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||||
self.validate_ordertypes(config.get('order_types', {}))
|
self.validate_ordertypes(config.get('order_types', {}))
|
||||||
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
||||||
self.validate_required_startup_candles(config.get('startup_candle_count', 0),
|
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||||
config.get('timeframe', ''))
|
config.get('startup_candle_count', 0), config.get('timeframe', ''))
|
||||||
|
|
||||||
# Converts the interval provided in minutes in config to seconds
|
# Converts the interval provided in minutes in config to seconds
|
||||||
self.markets_refresh_interval: int = exchange_config.get(
|
self.markets_refresh_interval: int = exchange_config.get(
|
||||||
@ -471,16 +471,29 @@ class Exchange:
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Time in force policies are not supported for {self.name} yet.')
|
f'Time in force policies are not supported for {self.name} yet.')
|
||||||
|
|
||||||
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> None:
|
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
|
||||||
"""
|
"""
|
||||||
Checks if required startup_candles is more than ohlcv_candle_limit().
|
Checks if required startup_candles is more than ohlcv_candle_limit().
|
||||||
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
|
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
|
||||||
"""
|
"""
|
||||||
candle_limit = self.ohlcv_candle_limit(timeframe)
|
candle_limit = self.ohlcv_candle_limit(timeframe)
|
||||||
if startup_candles + 5 > candle_limit:
|
# Require one more candle - to account for the still open candle.
|
||||||
|
candle_count = startup_candles + 1
|
||||||
|
# Allow 5 calls to the exchange per pair
|
||||||
|
required_candle_call_count = int(
|
||||||
|
(candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1))
|
||||||
|
|
||||||
|
if required_candle_call_count > 5:
|
||||||
|
# Only allow 5 calls per pair to somewhat limit the impact
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"This strategy requires {startup_candles} candles to start. "
|
f"This strategy requires {startup_candles} candles to start, which is more than 5x "
|
||||||
f"{self.name} only provides {candle_limit - 5} for {timeframe}.")
|
f"the amount of candles {self.name} provides for {timeframe}.")
|
||||||
|
|
||||||
|
if required_candle_call_count > 1:
|
||||||
|
logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. "
|
||||||
|
f"This can result in slower operations for the bot. Please check "
|
||||||
|
f"if you really need {startup_candles} candles for your strategy")
|
||||||
|
return required_candle_call_count
|
||||||
|
|
||||||
def exchange_has(self, endpoint: str) -> bool:
|
def exchange_has(self, endpoint: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -672,16 +685,20 @@ class Exchange:
|
|||||||
if not self.exchange_has('fetchL2OrderBook'):
|
if not self.exchange_has('fetchL2OrderBook'):
|
||||||
return True
|
return True
|
||||||
ob = self.fetch_l2_order_book(pair, 1)
|
ob = self.fetch_l2_order_book(pair, 1)
|
||||||
if side == 'buy':
|
try:
|
||||||
price = ob['asks'][0][0]
|
if side == 'buy':
|
||||||
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
|
price = ob['asks'][0][0]
|
||||||
if limit >= price:
|
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
|
||||||
return True
|
if limit >= price:
|
||||||
else:
|
return True
|
||||||
price = ob['bids'][0][0]
|
else:
|
||||||
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
|
price = ob['bids'][0][0]
|
||||||
if limit <= price:
|
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
|
||||||
return True
|
if limit <= price:
|
||||||
|
return True
|
||||||
|
except IndexError:
|
||||||
|
# Ignore empty orderbooks when filling - can be filled with the next iteration.
|
||||||
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]:
|
def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@ -1205,9 +1222,11 @@ class Exchange:
|
|||||||
:param since_ms: Timestamp in milliseconds to get history from
|
:param since_ms: Timestamp in milliseconds to get history from
|
||||||
:return: List with candle (OHLCV) data
|
:return: List with candle (OHLCV) data
|
||||||
"""
|
"""
|
||||||
return asyncio.get_event_loop().run_until_complete(
|
pair, timeframe, data = asyncio.get_event_loop().run_until_complete(
|
||||||
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
||||||
since_ms=since_ms, is_new_pair=is_new_pair))
|
since_ms=since_ms, is_new_pair=is_new_pair))
|
||||||
|
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
||||||
|
return data
|
||||||
|
|
||||||
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
|
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
|
||||||
since_ms: int) -> DataFrame:
|
since_ms: int) -> DataFrame:
|
||||||
@ -1223,8 +1242,9 @@ class Exchange:
|
|||||||
drop_incomplete=self._ohlcv_partial_candle)
|
drop_incomplete=self._ohlcv_partial_candle)
|
||||||
|
|
||||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
since_ms: int, is_new_pair: bool
|
since_ms: int, is_new_pair: bool = False,
|
||||||
) -> List:
|
raise_: bool = False
|
||||||
|
) -> Tuple[str, str, List]:
|
||||||
"""
|
"""
|
||||||
Download historic ohlcv
|
Download historic ohlcv
|
||||||
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
|
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
|
||||||
@ -1247,16 +1267,18 @@ class Exchange:
|
|||||||
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
||||||
for res in results:
|
for res in results:
|
||||||
if isinstance(res, Exception):
|
if isinstance(res, Exception):
|
||||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||||
|
if raise_:
|
||||||
|
raise
|
||||||
continue
|
continue
|
||||||
# Deconstruct tuple if it's not an exception
|
else:
|
||||||
p, _, new_data = res
|
# Deconstruct tuple if it's not an exception
|
||||||
if p == pair:
|
p, _, new_data = res
|
||||||
data.extend(new_data)
|
if p == pair:
|
||||||
|
data.extend(new_data)
|
||||||
# Sort data again after extending the result - above calls return in "async order"
|
# Sort data again after extending the result - above calls return in "async order"
|
||||||
data = sorted(data, key=lambda x: x[0])
|
data = sorted(data, key=lambda x: x[0])
|
||||||
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
return pair, timeframe, data
|
||||||
return data
|
|
||||||
|
|
||||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||||
since_ms: Optional[int] = None, cache: bool = True
|
since_ms: Optional[int] = None, cache: bool = True
|
||||||
@ -1276,10 +1298,22 @@ class Exchange:
|
|||||||
cached_pairs = []
|
cached_pairs = []
|
||||||
# Gather coroutines to run
|
# Gather coroutines to run
|
||||||
for pair, timeframe in set(pair_list):
|
for pair, timeframe in set(pair_list):
|
||||||
if (((pair, timeframe) not in self._klines)
|
if ((pair, timeframe) not in self._klines or not cache
|
||||||
or self._now_is_time_to_refresh(pair, timeframe)):
|
or self._now_is_time_to_refresh(pair, timeframe)):
|
||||||
input_coroutines.append(self._async_get_candle_history(pair, timeframe,
|
if not since_ms and self.required_candle_call_count > 1:
|
||||||
since_ms=since_ms))
|
# Multiple calls for one pair - to get more history
|
||||||
|
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
||||||
|
move_to = one_call * self.required_candle_call_count
|
||||||
|
now = timeframe_to_next_date(timeframe)
|
||||||
|
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
|
||||||
|
|
||||||
|
if since_ms:
|
||||||
|
input_coroutines.append(self._async_get_historic_ohlcv(
|
||||||
|
pair, timeframe, since_ms=since_ms, raise_=True))
|
||||||
|
else:
|
||||||
|
# One call ... "regular" refresh
|
||||||
|
input_coroutines.append(self._async_get_candle_history(
|
||||||
|
pair, timeframe, since_ms=since_ms))
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
|
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
|
||||||
@ -1287,27 +1321,30 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
cached_pairs.append((pair, timeframe))
|
cached_pairs.append((pair, timeframe))
|
||||||
|
|
||||||
results = asyncio.get_event_loop().run_until_complete(
|
|
||||||
asyncio.gather(*input_coroutines, return_exceptions=True))
|
|
||||||
|
|
||||||
results_df = {}
|
results_df = {}
|
||||||
# handle caching
|
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
|
||||||
for res in results:
|
for input_coro in chunks(input_coroutines, 100):
|
||||||
if isinstance(res, Exception):
|
results = asyncio.get_event_loop().run_until_complete(
|
||||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
asyncio.gather(*input_coro, return_exceptions=True))
|
||||||
continue
|
|
||||||
# Deconstruct tuple (has 3 elements)
|
# handle caching
|
||||||
pair, timeframe, ticks = res
|
for res in results:
|
||||||
# keeping last candle time as last refreshed time of the pair
|
if isinstance(res, Exception):
|
||||||
if ticks:
|
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||||
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
|
continue
|
||||||
# keeping parsed dataframe in cache
|
# Deconstruct tuple (has 3 elements)
|
||||||
ohlcv_df = ohlcv_to_dataframe(
|
pair, timeframe, ticks = res
|
||||||
ticks, timeframe, pair=pair, fill_missing=True,
|
# keeping last candle time as last refreshed time of the pair
|
||||||
drop_incomplete=self._ohlcv_partial_candle)
|
if ticks:
|
||||||
results_df[(pair, timeframe)] = ohlcv_df
|
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
|
||||||
if cache:
|
# keeping parsed dataframe in cache
|
||||||
self._klines[(pair, timeframe)] = ohlcv_df
|
ohlcv_df = ohlcv_to_dataframe(
|
||||||
|
ticks, timeframe, pair=pair, fill_missing=True,
|
||||||
|
drop_incomplete=self._ohlcv_partial_candle)
|
||||||
|
results_df[(pair, timeframe)] = ohlcv_df
|
||||||
|
if cache:
|
||||||
|
self._klines[(pair, timeframe)] = ohlcv_df
|
||||||
|
|
||||||
# Return cached klines
|
# Return cached klines
|
||||||
for pair, timeframe in cached_pairs:
|
for pair, timeframe in cached_pairs:
|
||||||
results_df[(pair, timeframe)] = self.klines((pair, timeframe), copy=False)
|
results_df[(pair, timeframe)] = self.klines((pair, timeframe), copy=False)
|
||||||
@ -1534,7 +1571,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
|
|||||||
|
|
||||||
|
|
||||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||||
return exchange_name in ['bittrex', 'binance', 'kraken']
|
return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okex']
|
||||||
|
|
||||||
|
|
||||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
""" Kucoin exchange subclass """
|
"""Kucoin exchange subclass."""
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
@ -9,9 +9,9 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class Kucoin(Exchange):
|
class Kucoin(Exchange):
|
||||||
"""
|
"""Kucoin exchange class.
|
||||||
Kucoin exchange class. Contains adjustments needed for Freqtrade to work
|
|
||||||
with this exchange.
|
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||||
|
|
||||||
Please note that this exchange is not included in the list of exchanges
|
Please note that this exchange is not included in the list of exchanges
|
||||||
officially supported by the Freqtrade development team. So some features
|
officially supported by the Freqtrade development team. So some features
|
||||||
|
18
freqtrade/exchange/okex.py
Normal file
18
freqtrade/exchange/okex.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Okex(Exchange):
|
||||||
|
"""Okex exchange class.
|
||||||
|
|
||||||
|
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ft_has: Dict = {
|
||||||
|
"ohlcv_candle_limit": 100,
|
||||||
|
}
|
@ -193,19 +193,20 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
def check_for_open_trades(self):
|
def check_for_open_trades(self):
|
||||||
"""
|
"""
|
||||||
Notify the user when the bot is stopped
|
Notify the user when the bot is stopped (not reloaded)
|
||||||
and there are still open trades active.
|
and there are still open trades active.
|
||||||
"""
|
"""
|
||||||
open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all()
|
open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all()
|
||||||
|
|
||||||
if len(open_trades) != 0:
|
if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG:
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.WARNING,
|
'type': RPCMessageType.WARNING,
|
||||||
'status': f"{len(open_trades)} open trades active.\n\n"
|
'status':
|
||||||
f"Handle these trades manually on {self.exchange.name}, "
|
f"{len(open_trades)} open trades active.\n\n"
|
||||||
f"or '/start' the bot again and use '/stopbuy' "
|
f"Handle these trades manually on {self.exchange.name}, "
|
||||||
f"to handle open trades gracefully. \n"
|
f"or '/start' the bot again and use '/stopbuy' "
|
||||||
f"{'Trades are simulated.' if self.config['dry_run'] else ''}",
|
f"to handle open trades gracefully. \n"
|
||||||
|
f"{'Note: Trades are simulated (dry run).' if self.config['dry_run'] else ''}",
|
||||||
}
|
}
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
@ -277,7 +278,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if order:
|
if order:
|
||||||
logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.")
|
logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.")
|
||||||
self.update_trade_state(trade, order.order_id,
|
self.update_trade_state(trade, order.order_id,
|
||||||
stoploss_order=order.ft_order_side == 'stoploss')
|
stoploss_order=order.ft_order_side == 'stoploss',
|
||||||
|
send_msg=False)
|
||||||
|
|
||||||
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
|
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
@ -285,7 +287,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order = trade.select_order('buy', False)
|
order = trade.select_order('buy', False)
|
||||||
if order:
|
if order:
|
||||||
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
||||||
self.update_trade_state(trade, order.order_id)
|
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||||
|
|
||||||
def handle_insufficient_funds(self, trade: Trade):
|
def handle_insufficient_funds(self, trade: Trade):
|
||||||
"""
|
"""
|
||||||
@ -307,7 +309,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order = trade.select_order('buy', False)
|
order = trade.select_order('buy', False)
|
||||||
if order:
|
if order:
|
||||||
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
||||||
self.update_trade_state(trade, order.order_id)
|
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||||
|
|
||||||
def refind_lost_order(self, trade):
|
def refind_lost_order(self, trade):
|
||||||
"""
|
"""
|
||||||
@ -420,7 +422,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# running get_signal on historical data fetched
|
# running get_signal on historical data fetched
|
||||||
(buy, sell, buy_tag) = self.strategy.get_signal(
|
(buy, sell, buy_tag, _) = self.strategy.get_signal(
|
||||||
pair,
|
pair,
|
||||||
self.strategy.timeframe,
|
self.strategy.timeframe,
|
||||||
analyzed_df
|
analyzed_df
|
||||||
@ -465,8 +467,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None,
|
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, *,
|
||||||
forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool:
|
ordertype: Optional[str] = None, buy_tag: Optional[str] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a limit buy for the given pair
|
Executes a limit buy for the given pair
|
||||||
:param pair: pair for which we want to create a LIMIT_BUY
|
:param pair: pair for which we want to create a LIMIT_BUY
|
||||||
@ -500,7 +502,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
pair=pair, current_time=datetime.now(timezone.utc),
|
pair=pair, current_time=datetime.now(timezone.utc),
|
||||||
current_rate=enter_limit_requested, proposed_stake=stake_amount,
|
current_rate=enter_limit_requested, proposed_stake=stake_amount,
|
||||||
min_stake=min_stake_amount, max_stake=max_stake_amount)
|
min_stake=min_stake_amount, max_stake=max_stake_amount)
|
||||||
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||||
|
|
||||||
if not stake_amount:
|
if not stake_amount:
|
||||||
return False
|
return False
|
||||||
@ -509,10 +511,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
f"{stake_amount} ...")
|
f"{stake_amount} ...")
|
||||||
|
|
||||||
amount = stake_amount / enter_limit_requested
|
amount = stake_amount / enter_limit_requested
|
||||||
order_type = self.strategy.order_types['buy']
|
order_type = ordertype or self.strategy.order_types['buy']
|
||||||
if forcebuy:
|
|
||||||
# Forcebuy can define a different ordertype
|
|
||||||
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
|
||||||
|
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||||
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||||
@ -580,10 +579,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
)
|
)
|
||||||
trade.orders.append(order_obj)
|
trade.orders.append(order_obj)
|
||||||
|
|
||||||
# Update fees if order is closed
|
|
||||||
if order_status == 'closed':
|
|
||||||
self.update_trade_state(trade, order_id, order)
|
|
||||||
|
|
||||||
Trade.query.session.add(trade)
|
Trade.query.session.add(trade)
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
@ -592,19 +587,25 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
self._notify_enter(trade, order_type)
|
self._notify_enter(trade, order_type)
|
||||||
|
|
||||||
|
# Update fees if order is closed
|
||||||
|
if order_status == 'closed':
|
||||||
|
self.update_trade_state(trade, order_id, order)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _notify_enter(self, trade: Trade, order_type: str) -> None:
|
def _notify_enter(self, trade: Trade, order_type: Optional[str] = None,
|
||||||
|
fill: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a buy occurred.
|
Sends rpc notification when a buy occurred.
|
||||||
"""
|
"""
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY,
|
'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY,
|
||||||
'buy_tag': trade.buy_tag,
|
'buy_tag': trade.buy_tag,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
'limit': trade.open_rate,
|
'limit': trade.open_rate, # Deprecated (?)
|
||||||
|
'open_rate': trade.open_rate,
|
||||||
'order_type': order_type,
|
'order_type': order_type,
|
||||||
'stake_amount': trade.stake_amount,
|
'stake_amount': trade.stake_amount,
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
@ -643,22 +644,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Send the message
|
# Send the message
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def _notify_enter_fill(self, trade: Trade) -> None:
|
|
||||||
msg = {
|
|
||||||
'trade_id': trade.id,
|
|
||||||
'type': RPCMessageType.BUY_FILL,
|
|
||||||
'buy_tag': trade.buy_tag,
|
|
||||||
'exchange': self.exchange.name.capitalize(),
|
|
||||||
'pair': trade.pair,
|
|
||||||
'open_rate': trade.open_rate,
|
|
||||||
'stake_amount': trade.stake_amount,
|
|
||||||
'stake_currency': self.config['stake_currency'],
|
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
|
||||||
'amount': trade.amount,
|
|
||||||
'open_date': trade.open_date,
|
|
||||||
}
|
|
||||||
self.rpc.send_msg(msg)
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# SELL / exit positions / close trades logic and methods
|
# SELL / exit positions / close trades logic and methods
|
||||||
#
|
#
|
||||||
@ -700,21 +685,22 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.debug('Handling %s ...', trade)
|
logger.debug('Handling %s ...', trade)
|
||||||
|
|
||||||
(buy, sell) = (False, False)
|
(buy, sell) = (False, False)
|
||||||
|
exit_tag = None
|
||||||
|
|
||||||
if (self.config.get('use_sell_signal', True) or
|
if (self.config.get('use_sell_signal', True) or
|
||||||
self.config.get('ignore_roi_if_buy_signal', False)):
|
self.config.get('ignore_roi_if_buy_signal', False)):
|
||||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
||||||
self.strategy.timeframe)
|
self.strategy.timeframe)
|
||||||
|
|
||||||
(buy, sell, _) = self.strategy.get_signal(
|
(buy, sell, _, exit_tag) = self.strategy.get_signal(
|
||||||
trade.pair,
|
trade.pair,
|
||||||
self.strategy.timeframe,
|
self.strategy.timeframe,
|
||||||
analyzed_df
|
analyzed_df
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug('checking sell')
|
logger.debug('checking sell')
|
||||||
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
||||||
if self._check_and_execute_exit(trade, exit_rate, buy, sell):
|
if self._check_and_execute_exit(trade, sell_rate, buy, sell, exit_tag):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.debug('Found no sell signal for %s.', trade)
|
logger.debug('Found no sell signal for %s.', trade)
|
||||||
@ -852,18 +838,21 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
f"for pair {trade.pair}.")
|
f"for pair {trade.pair}.")
|
||||||
|
|
||||||
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
||||||
buy: bool, sell: bool) -> bool:
|
buy: bool, sell: bool, exit_tag: Optional[str]) -> bool:
|
||||||
"""
|
"""
|
||||||
Check and execute exit
|
Check and execute exit
|
||||||
"""
|
"""
|
||||||
|
|
||||||
should_sell = self.strategy.should_sell(
|
should_sell = self.strategy.should_sell(
|
||||||
trade, exit_rate, datetime.now(timezone.utc), buy, sell,
|
trade, exit_rate, datetime.now(timezone.utc), buy, sell,
|
||||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
if should_sell.sell_flag:
|
if should_sell.sell_flag:
|
||||||
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
logger.info(
|
||||||
self.execute_trade_exit(trade, exit_rate, should_sell)
|
f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. '
|
||||||
|
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
||||||
|
self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag=exit_tag)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -916,6 +905,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade=trade,
|
trade=trade,
|
||||||
order=order))):
|
order=order))):
|
||||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
|
canceled_count = trade.get_exit_order_count()
|
||||||
|
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||||
|
if max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||||
|
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
|
||||||
|
f'timed out {max_timeouts} times.')
|
||||||
|
self.execute_trade_exit(trade, order.get('price'), sell_reason=SellCheckTuple(
|
||||||
|
sell_type=SellType.EMERGENCY_SELL))
|
||||||
|
|
||||||
def cancel_all_open_orders(self) -> None:
|
def cancel_all_open_orders(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1064,7 +1060,15 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
||||||
|
|
||||||
def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
|
def execute_trade_exit(
|
||||||
|
self,
|
||||||
|
trade: Trade,
|
||||||
|
limit: float,
|
||||||
|
sell_reason: SellCheckTuple,
|
||||||
|
*,
|
||||||
|
exit_tag: Optional[str] = None,
|
||||||
|
ordertype: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a trade exit for the given trade and limit
|
Executes a trade exit for the given trade and limit
|
||||||
:param trade: Trade instance
|
:param trade: Trade instance
|
||||||
@ -1102,14 +1106,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||||
|
|
||||||
order_type = self.strategy.order_types[sell_type]
|
order_type = ordertype or self.strategy.order_types[sell_type]
|
||||||
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
|
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
|
||||||
# Emergency sells (default to market!)
|
# Emergency sells (default to market!)
|
||||||
order_type = self.strategy.order_types.get("emergencysell", "market")
|
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||||
if sell_reason.sell_type == SellType.FORCE_SELL:
|
|
||||||
# Force sells (default to the sell_type defined in the strategy,
|
|
||||||
# but we allow this value to be changed)
|
|
||||||
order_type = self.strategy.order_types.get("forcesell", order_type)
|
|
||||||
|
|
||||||
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
||||||
time_in_force = self.strategy.order_time_in_force['sell']
|
time_in_force = self.strategy.order_time_in_force['sell']
|
||||||
@ -1140,17 +1140,17 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.open_order_id = order['id']
|
trade.open_order_id = order['id']
|
||||||
trade.sell_order_status = ''
|
trade.sell_order_status = ''
|
||||||
trade.close_rate_requested = limit
|
trade.close_rate_requested = limit
|
||||||
trade.sell_reason = sell_reason.sell_reason
|
trade.sell_reason = exit_tag or sell_reason.sell_reason
|
||||||
# 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()
|
|
||||||
|
|
||||||
# Lock pair for one candle to prevent immediate re-buys
|
# Lock pair for one candle to prevent immediate re-buys
|
||||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||||
reason='Auto lock')
|
reason='Auto lock')
|
||||||
|
|
||||||
self._notify_exit(trade, order_type)
|
self._notify_exit(trade, order_type)
|
||||||
|
# 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()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -1181,6 +1181,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit_trade,
|
'profit_amount': profit_trade,
|
||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
|
'buy_tag': trade.buy_tag,
|
||||||
'sell_reason': trade.sell_reason,
|
'sell_reason': trade.sell_reason,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date or datetime.utcnow(),
|
'close_date': trade.close_date or datetime.utcnow(),
|
||||||
@ -1224,6 +1225,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit_trade,
|
'profit_amount': profit_trade,
|
||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
|
'buy_tag': trade.buy_tag,
|
||||||
'sell_reason': trade.sell_reason,
|
'sell_reason': trade.sell_reason,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date or datetime.now(timezone.utc),
|
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||||
@ -1245,13 +1247,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
#
|
#
|
||||||
|
|
||||||
def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None,
|
def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None,
|
||||||
stoploss_order: bool = False) -> bool:
|
stoploss_order: bool = False, send_msg: bool = True) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks trades with open orders and updates the amount if necessary
|
Checks trades with open orders and updates the amount if necessary
|
||||||
Handles closing both buy and sell orders.
|
Handles closing both buy and sell orders.
|
||||||
:param trade: Trade object of the trade we're analyzing
|
:param trade: Trade object of the trade we're analyzing
|
||||||
:param order_id: Order-id of the order we're analyzing
|
:param order_id: Order-id of the order we're analyzing
|
||||||
:param action_order: Already acquired order object
|
:param action_order: Already acquired order object
|
||||||
|
:param send_msg: Send notification - should always be True except in "recovery" methods
|
||||||
:return: True if order has been cancelled without being filled partially, False otherwise
|
:return: True if order has been cancelled without being filled partially, False otherwise
|
||||||
"""
|
"""
|
||||||
if not order_id:
|
if not order_id:
|
||||||
@ -1270,6 +1273,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
trade.update_order(order)
|
trade.update_order(order)
|
||||||
|
|
||||||
|
if self.exchange.check_order_canceled_empty(order):
|
||||||
|
# Trade has been cancelled on exchange
|
||||||
|
# Handling of this will happen in check_handle_timedout.
|
||||||
|
return True
|
||||||
|
|
||||||
# Try update amount (binance-fix)
|
# Try update amount (binance-fix)
|
||||||
try:
|
try:
|
||||||
new_amount = self.get_real_amount(trade, order)
|
new_amount = self.get_real_amount(trade, order)
|
||||||
@ -1281,22 +1289,18 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
logger.warning("Could not update trade amount: %s", exception)
|
logger.warning("Could not update trade amount: %s", exception)
|
||||||
|
|
||||||
if self.exchange.check_order_canceled_empty(order):
|
|
||||||
# Trade has been cancelled on exchange
|
|
||||||
# Handling of this will happen in check_handle_timeout.
|
|
||||||
return True
|
|
||||||
trade.update(order)
|
trade.update(order)
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
# Updating wallets when order is closed
|
# Updating wallets when order is closed
|
||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
if not stoploss_order and not trade.open_order_id:
|
if send_msg and not stoploss_order and not trade.open_order_id:
|
||||||
self._notify_exit(trade, '', True)
|
self._notify_exit(trade, '', True)
|
||||||
self.handle_protections(trade.pair)
|
self.handle_protections(trade.pair)
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
elif not trade.open_order_id:
|
elif send_msg and not trade.open_order_id:
|
||||||
# Buy fill
|
# Buy fill
|
||||||
self._notify_enter_fill(trade)
|
self._notify_enter(trade, fill=True)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -1361,14 +1365,17 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
return self.apply_fee_conditional(trade, trade_base_currency,
|
return self.apply_fee_conditional(trade, trade_base_currency,
|
||||||
amount=order_amount, fee_abs=fee_cost)
|
amount=order_amount, fee_abs=fee_cost)
|
||||||
return order_amount
|
return order_amount
|
||||||
return self.fee_detection_from_trades(trade, order, order_amount)
|
return self.fee_detection_from_trades(trade, order, order_amount, order.get('trades', []))
|
||||||
|
|
||||||
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float:
|
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float,
|
||||||
|
trades: List) -> float:
|
||||||
"""
|
"""
|
||||||
fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee.
|
fee-detection fallback to Trades.
|
||||||
|
Either uses provided trades list or the result of fetch_my_trades to get correct fee.
|
||||||
"""
|
"""
|
||||||
trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order),
|
if not trades:
|
||||||
trade.pair, trade.open_date)
|
trades = self.exchange.get_trades_for_order(
|
||||||
|
self.exchange.get_order_id_conditional(order), trade.pair, trade.open_date)
|
||||||
|
|
||||||
if len(trades) == 0:
|
if len(trades) == 0:
|
||||||
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
|
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
|
||||||
|
@ -44,6 +44,7 @@ SELL_IDX = 4
|
|||||||
LOW_IDX = 5
|
LOW_IDX = 5
|
||||||
HIGH_IDX = 6
|
HIGH_IDX = 6
|
||||||
BUY_TAG_IDX = 7
|
BUY_TAG_IDX = 7
|
||||||
|
EXIT_TAG_IDX = 8
|
||||||
|
|
||||||
|
|
||||||
class Backtesting:
|
class Backtesting:
|
||||||
@ -66,7 +67,7 @@ class Backtesting:
|
|||||||
self.all_results: Dict[str, Dict] = {}
|
self.all_results: Dict[str, Dict] = {}
|
||||||
|
|
||||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||||
self.dataprovider = DataProvider(self.config, None)
|
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||||
|
|
||||||
if self.config.get('strategy_list', None):
|
if self.config.get('strategy_list', None):
|
||||||
for strat in list(self.config['strategy_list']):
|
for strat in list(self.config['strategy_list']):
|
||||||
@ -88,7 +89,8 @@ class Backtesting:
|
|||||||
self.init_backtest_detail()
|
self.init_backtest_detail()
|
||||||
self.pairlists = PairListManager(self.exchange, self.config)
|
self.pairlists = PairListManager(self.exchange, self.config)
|
||||||
if 'VolumePairList' in self.pairlists.name_list:
|
if 'VolumePairList' in self.pairlists.name_list:
|
||||||
raise OperationalException("VolumePairList not allowed for backtesting.")
|
raise OperationalException("VolumePairList not allowed for backtesting. "
|
||||||
|
"Please use StaticPairlist instead.")
|
||||||
if 'PerformanceFilter' in self.pairlists.name_list:
|
if 'PerformanceFilter' in self.pairlists.name_list:
|
||||||
raise OperationalException("PerformanceFilter not allowed for backtesting.")
|
raise OperationalException("PerformanceFilter not allowed for backtesting.")
|
||||||
|
|
||||||
@ -247,7 +249,7 @@ class Backtesting:
|
|||||||
"""
|
"""
|
||||||
# Every change to this headers list must evaluate further usages of the resulting tuple
|
# Every change to this headers list must evaluate further usages of the resulting tuple
|
||||||
# and eventually change the constants for indexes at the top
|
# and eventually change the constants for indexes at the top
|
||||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag']
|
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag', 'exit_tag']
|
||||||
data: Dict = {}
|
data: Dict = {}
|
||||||
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
||||||
|
|
||||||
@ -259,6 +261,7 @@ class Backtesting:
|
|||||||
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
|
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
|
||||||
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
|
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
|
||||||
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
|
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
|
||||||
|
pair_data.loc[:, 'exit_tag'] = None # cleanup if exit_tag is exist
|
||||||
|
|
||||||
df_analyzed = self.strategy.advise_sell(
|
df_analyzed = self.strategy.advise_sell(
|
||||||
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy()
|
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy()
|
||||||
@ -270,6 +273,7 @@ class Backtesting:
|
|||||||
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
|
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
|
||||||
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1)
|
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1)
|
||||||
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
|
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
|
||||||
|
df_analyzed.loc[:, 'exit_tag'] = df_analyzed.loc[:, 'exit_tag'].shift(1)
|
||||||
|
|
||||||
# Update dataprovider cache
|
# Update dataprovider cache
|
||||||
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
|
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
|
||||||
@ -312,7 +316,9 @@ class Backtesting:
|
|||||||
# Worst case: price ticks tiny bit above open and dives down.
|
# Worst case: price ticks tiny bit above open and dives down.
|
||||||
stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct))
|
stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct))
|
||||||
assert stop_rate < sell_row[HIGH_IDX]
|
assert stop_rate < sell_row[HIGH_IDX]
|
||||||
return stop_rate
|
# Limit lower-end to candle low to avoid sells below the low.
|
||||||
|
# This still remains "worst case" - but "worst realistic case".
|
||||||
|
return max(sell_row[LOW_IDX], stop_rate)
|
||||||
|
|
||||||
# Set close_rate to stoploss
|
# Set close_rate to stoploss
|
||||||
return trade.stop_loss
|
return trade.stop_loss
|
||||||
@ -357,7 +363,7 @@ class Backtesting:
|
|||||||
|
|
||||||
if sell.sell_flag:
|
if sell.sell_flag:
|
||||||
trade.close_date = sell_candle_time
|
trade.close_date = sell_candle_time
|
||||||
trade.sell_reason = sell.sell_reason
|
|
||||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||||
# call the custom exit price,with default value as previous closerate
|
# call the custom exit price,with default value as previous closerate
|
||||||
@ -378,6 +384,17 @@ class Backtesting:
|
|||||||
current_time=sell_candle_time):
|
current_time=sell_candle_time):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
trade.sell_reason = sell.sell_reason
|
||||||
|
|
||||||
|
# Checks and adds an exit tag, after checking that the length of the
|
||||||
|
# sell_row has the length for an exit tag column
|
||||||
|
if(
|
||||||
|
len(sell_row) > EXIT_TAG_IDX
|
||||||
|
and sell_row[EXIT_TAG_IDX] is not None
|
||||||
|
and len(sell_row[EXIT_TAG_IDX]) > 0
|
||||||
|
):
|
||||||
|
trade.sell_reason = sell_row[EXIT_TAG_IDX]
|
||||||
|
|
||||||
trade.close(closerate, show_msg=False)
|
trade.close(closerate, show_msg=False)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
@ -392,7 +409,7 @@ class Backtesting:
|
|||||||
detail_data = detail_data.loc[
|
detail_data = detail_data.loc[
|
||||||
(detail_data['date'] >= sell_candle_time) &
|
(detail_data['date'] >= sell_candle_time) &
|
||||||
(detail_data['date'] < sell_candle_end)
|
(detail_data['date'] < sell_candle_end)
|
||||||
].copy()
|
].copy()
|
||||||
if len(detail_data) == 0:
|
if len(detail_data) == 0:
|
||||||
# Fall back to "regular" data if no detail data was found for this candle
|
# Fall back to "regular" data if no detail data was found for this candle
|
||||||
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
||||||
@ -427,7 +444,7 @@ class Backtesting:
|
|||||||
default_retval=stake_amount)(
|
default_retval=stake_amount)(
|
||||||
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate,
|
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate,
|
||||||
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount)
|
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount)
|
||||||
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||||
|
|
||||||
if not stake_amount:
|
if not stake_amount:
|
||||||
return None
|
return None
|
||||||
|
@ -45,7 +45,7 @@ progressbar.streams.wrap_stdout()
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
INITIAL_POINTS = 5
|
INITIAL_POINTS = 30
|
||||||
|
|
||||||
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
|
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
|
||||||
# in the skopt model queue, to optimize memory consumption
|
# in the skopt model queue, to optimize memory consumption
|
||||||
|
64
freqtrade/optimize/hyperopt_loss_calmar.py
Normal file
64
freqtrade/optimize/hyperopt_loss_calmar.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
CalmarHyperOptLoss
|
||||||
|
|
||||||
|
This module defines the alternative HyperOptLoss class which can be used for
|
||||||
|
Hyperoptimization.
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from math import sqrt as msqrt
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.data.btanalysis import calculate_max_drawdown
|
||||||
|
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||||
|
|
||||||
|
|
||||||
|
class CalmarHyperOptLoss(IHyperOptLoss):
|
||||||
|
"""
|
||||||
|
Defines the loss function for hyperopt.
|
||||||
|
|
||||||
|
This implementation uses the Calmar Ratio calculation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def hyperopt_loss_function(
|
||||||
|
results: DataFrame,
|
||||||
|
trade_count: int,
|
||||||
|
min_date: datetime,
|
||||||
|
max_date: datetime,
|
||||||
|
config: Dict,
|
||||||
|
processed: Dict[str, DataFrame],
|
||||||
|
backtest_stats: Dict[str, Any],
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Objective function, returns smaller number for more optimal results.
|
||||||
|
|
||||||
|
Uses Calmar Ratio calculation.
|
||||||
|
"""
|
||||||
|
total_profit = backtest_stats["profit_total"]
|
||||||
|
days_period = (max_date - min_date).days
|
||||||
|
|
||||||
|
# adding slippage of 0.1% per trade
|
||||||
|
total_profit = total_profit - 0.0005
|
||||||
|
expected_returns_mean = total_profit.sum() / days_period * 100
|
||||||
|
|
||||||
|
# calculate max drawdown
|
||||||
|
try:
|
||||||
|
_, _, _, high_val, low_val = calculate_max_drawdown(
|
||||||
|
results, value_col="profit_abs"
|
||||||
|
)
|
||||||
|
max_drawdown = (high_val - low_val) / high_val
|
||||||
|
except ValueError:
|
||||||
|
max_drawdown = 0
|
||||||
|
|
||||||
|
if max_drawdown != 0:
|
||||||
|
calmar_ratio = expected_returns_mean / max_drawdown * msqrt(365)
|
||||||
|
else:
|
||||||
|
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
|
||||||
|
calmar_ratio = -20.0
|
||||||
|
|
||||||
|
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||||
|
return -calmar_ratio
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
@ -64,10 +63,11 @@ class HyperoptTools():
|
|||||||
'export_time': datetime.now(timezone.utc),
|
'export_time': datetime.now(timezone.utc),
|
||||||
}
|
}
|
||||||
logger.info(f"Dumping parameters to {filename}")
|
logger.info(f"Dumping parameters to {filename}")
|
||||||
rapidjson.dump(final_params, filename.open('w'), indent=2,
|
with filename.open('w') as f:
|
||||||
default=hyperopt_serializer,
|
rapidjson.dump(final_params, f, indent=2,
|
||||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
default=hyperopt_serializer,
|
||||||
)
|
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def try_export_params(config: Dict[str, Any], strategy_name: str, params: Dict):
|
def try_export_params(config: Dict[str, Any], strategy_name: str, params: Dict):
|
||||||
@ -284,10 +284,10 @@ class HyperoptTools():
|
|||||||
return (f"{results_metrics['total_trades']:6d} trades. "
|
return (f"{results_metrics['total_trades']:6d} trades. "
|
||||||
f"{results_metrics['wins']}/{results_metrics['draws']}"
|
f"{results_metrics['wins']}/{results_metrics['draws']}"
|
||||||
f"/{results_metrics['losses']} Wins/Draws/Losses. "
|
f"/{results_metrics['losses']} Wins/Draws/Losses. "
|
||||||
f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. "
|
f"Avg profit {results_metrics['profit_mean']:7.2%}. "
|
||||||
f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. "
|
f"Median profit {results_metrics['profit_median']:7.2%}. "
|
||||||
f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} "
|
f"Total profit {results_metrics['profit_total_abs']:11.8f} {stake_currency} "
|
||||||
f"({results_metrics['profit_total'] * 100: 7.2f}%). "
|
f"({results_metrics['profit_total']:8.2%}). "
|
||||||
f"Avg duration {results_metrics['holding_avg']} min."
|
f"Avg duration {results_metrics['holding_avg']} min."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
from numpy import int64
|
from numpy import int64
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame, to_datetime
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
|
||||||
@ -46,11 +46,11 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]:
|
|||||||
'.2f', 'd', 's', 's']
|
'.2f', 'd', 's', 's']
|
||||||
|
|
||||||
|
|
||||||
def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
|
def _get_line_header(first_column: str, stake_currency: str, direction: str = 'Buys') -> List[str]:
|
||||||
"""
|
"""
|
||||||
Generate header lines (goes in line with _generate_result_line())
|
Generate header lines (goes in line with _generate_result_line())
|
||||||
"""
|
"""
|
||||||
return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %',
|
return [first_column, direction, 'Avg Profit %', 'Cum Profit %',
|
||||||
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
||||||
'Win Draw Loss Win%']
|
'Win Draw Loss Win%']
|
||||||
|
|
||||||
@ -127,6 +127,38 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b
|
|||||||
return tabular_data
|
return tabular_data
|
||||||
|
|
||||||
|
|
||||||
|
def generate_tag_metrics(tag_type: str,
|
||||||
|
starting_balance: int,
|
||||||
|
results: DataFrame,
|
||||||
|
skip_nan: bool = False) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Generates and returns a list of metrics for the given tag trades and the results dataframe
|
||||||
|
:param starting_balance: Starting balance
|
||||||
|
:param results: Dataframe containing the backtest results
|
||||||
|
:param skip_nan: Print "left open" open trades
|
||||||
|
:return: List of Dicts containing the metrics per pair
|
||||||
|
"""
|
||||||
|
|
||||||
|
tabular_data = []
|
||||||
|
|
||||||
|
if tag_type in results.columns:
|
||||||
|
for tag, count in results[tag_type].value_counts().iteritems():
|
||||||
|
result = results[results[tag_type] == tag]
|
||||||
|
if skip_nan and result['profit_abs'].isnull().all():
|
||||||
|
continue
|
||||||
|
|
||||||
|
tabular_data.append(_generate_result_line(result, starting_balance, tag))
|
||||||
|
|
||||||
|
# Sort by total profit %:
|
||||||
|
tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)
|
||||||
|
|
||||||
|
# Append Total
|
||||||
|
tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL'))
|
||||||
|
return tabular_data
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Generate small table outlining Backtest results
|
Generate small table outlining Backtest results
|
||||||
@ -189,7 +221,6 @@ def generate_strategy_comparison(all_results: Dict) -> List[Dict]:
|
|||||||
|
|
||||||
|
|
||||||
def generate_edge_table(results: dict) -> str:
|
def generate_edge_table(results: dict) -> str:
|
||||||
|
|
||||||
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
|
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
|
||||||
tabular_data = []
|
tabular_data = []
|
||||||
headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
|
headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
|
||||||
@ -214,6 +245,41 @@ def generate_edge_table(results: dict) -> str:
|
|||||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def _get_resample_from_period(period: str) -> str:
|
||||||
|
if period == 'day':
|
||||||
|
return '1d'
|
||||||
|
if period == 'week':
|
||||||
|
return '1w'
|
||||||
|
if period == 'month':
|
||||||
|
return '1M'
|
||||||
|
raise ValueError(f"Period {period} is not supported.")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dict[str, Any]]:
|
||||||
|
results = DataFrame.from_records(trade_list)
|
||||||
|
if len(results) == 0:
|
||||||
|
return []
|
||||||
|
results['close_date'] = to_datetime(results['close_date'], utc=True)
|
||||||
|
resample_period = _get_resample_from_period(period)
|
||||||
|
resampled = results.resample(resample_period, on='close_date')
|
||||||
|
stats = []
|
||||||
|
for name, day in resampled:
|
||||||
|
profit_abs = day['profit_abs'].sum().round(10)
|
||||||
|
wins = sum(day['profit_abs'] > 0)
|
||||||
|
draws = sum(day['profit_abs'] == 0)
|
||||||
|
loses = sum(day['profit_abs'] < 0)
|
||||||
|
stats.append(
|
||||||
|
{
|
||||||
|
'date': name.strftime('%d/%m/%Y'),
|
||||||
|
'profit_abs': profit_abs,
|
||||||
|
'wins': wins,
|
||||||
|
'draws': draws,
|
||||||
|
'loses': loses
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
|
def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
|
||||||
""" Generate overall trade statistics """
|
""" Generate overall trade statistics """
|
||||||
if len(results) == 0:
|
if len(results) == 0:
|
||||||
@ -313,6 +379,10 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
|||||||
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
||||||
starting_balance=starting_balance,
|
starting_balance=starting_balance,
|
||||||
results=results, skip_nan=False)
|
results=results, skip_nan=False)
|
||||||
|
|
||||||
|
buy_tag_results = generate_tag_metrics("buy_tag", starting_balance=starting_balance,
|
||||||
|
results=results, skip_nan=False)
|
||||||
|
|
||||||
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
||||||
results=results)
|
results=results)
|
||||||
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
||||||
@ -329,15 +399,18 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
|||||||
results['open_timestamp'] = results['open_date'].view(int64) // 1e6
|
results['open_timestamp'] = results['open_date'].view(int64) // 1e6
|
||||||
results['close_timestamp'] = results['close_date'].view(int64) // 1e6
|
results['close_timestamp'] = results['close_date'].view(int64) // 1e6
|
||||||
|
|
||||||
backtest_days = (max_date - min_date).days
|
backtest_days = (max_date - min_date).days or 1
|
||||||
strat_stats = {
|
strat_stats = {
|
||||||
'trades': results.to_dict(orient='records'),
|
'trades': results.to_dict(orient='records'),
|
||||||
'locks': [lock.to_json() for lock in content['locks']],
|
'locks': [lock.to_json() for lock in content['locks']],
|
||||||
'best_pair': best_pair,
|
'best_pair': best_pair,
|
||||||
'worst_pair': worst_pair,
|
'worst_pair': worst_pair,
|
||||||
'results_per_pair': pair_results,
|
'results_per_pair': pair_results,
|
||||||
|
'results_per_buy_tag': buy_tag_results,
|
||||||
'sell_reason_summary': sell_reason_stats,
|
'sell_reason_summary': sell_reason_stats,
|
||||||
'left_open_trades': left_open_results,
|
'left_open_trades': left_open_results,
|
||||||
|
# 'days_breakdown_stats': days_breakdown_stats,
|
||||||
|
|
||||||
'total_trades': len(results),
|
'total_trades': len(results),
|
||||||
'total_volume': float(results['stake_amount'].sum()),
|
'total_volume': float(results['stake_amount'].sum()),
|
||||||
'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
|
'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
|
||||||
@ -354,7 +427,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
|||||||
'backtest_run_start_ts': content['backtest_start_time'],
|
'backtest_run_start_ts': content['backtest_start_time'],
|
||||||
'backtest_run_end_ts': content['backtest_end_time'],
|
'backtest_run_end_ts': content['backtest_end_time'],
|
||||||
|
|
||||||
'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
|
'trades_per_day': round(len(results) / backtest_days, 2),
|
||||||
'market_change': market_change,
|
'market_change': market_change,
|
||||||
'pairlist': list(btdata.keys()),
|
'pairlist': list(btdata.keys()),
|
||||||
'stake_amount': config['stake_amount'],
|
'stake_amount': config['stake_amount'],
|
||||||
@ -506,6 +579,59 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
|
|||||||
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||||
|
|
||||||
|
|
||||||
|
def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||||
|
"""
|
||||||
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
|
:param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
|
||||||
|
:param stake_currency: stake-currency - used to correctly name headers
|
||||||
|
:return: pretty printed table with tabulate as string
|
||||||
|
"""
|
||||||
|
if(tag_type == "buy_tag"):
|
||||||
|
headers = _get_line_header("TAG", stake_currency)
|
||||||
|
else:
|
||||||
|
headers = _get_line_header("TAG", stake_currency, 'Sells')
|
||||||
|
floatfmt = _get_line_floatfmt(stake_currency)
|
||||||
|
output = [
|
||||||
|
[
|
||||||
|
t['key'] if t['key'] is not None and len(
|
||||||
|
t['key']) > 0 else "OTHER",
|
||||||
|
t['trades'],
|
||||||
|
t['profit_mean_pct'],
|
||||||
|
t['profit_sum_pct'],
|
||||||
|
t['profit_total_abs'],
|
||||||
|
t['profit_total_pct'],
|
||||||
|
t['duration_avg'],
|
||||||
|
_generate_wins_draws_losses(
|
||||||
|
t['wins'],
|
||||||
|
t['draws'],
|
||||||
|
t['losses'])] for t in tag_results]
|
||||||
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
|
return tabulate(output, headers=headers,
|
||||||
|
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||||
|
|
||||||
|
|
||||||
|
def text_table_periodic_breakdown(days_breakdown_stats: List[Dict[str, Any]],
|
||||||
|
stake_currency: str, period: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate small table with Backtest results by days
|
||||||
|
:param days_breakdown_stats: Days breakdown metrics
|
||||||
|
:param stake_currency: Stakecurrency used
|
||||||
|
:return: pretty printed table with tabulate as string
|
||||||
|
"""
|
||||||
|
headers = [
|
||||||
|
period.capitalize(),
|
||||||
|
f'Tot Profit {stake_currency}',
|
||||||
|
'Wins',
|
||||||
|
'Draws',
|
||||||
|
'Losses',
|
||||||
|
]
|
||||||
|
output = [[
|
||||||
|
d['date'], round_coin_value(d['profit_abs'], stake_currency, False),
|
||||||
|
d['wins'], d['draws'], d['loses'],
|
||||||
|
] for d in days_breakdown_stats]
|
||||||
|
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||||
|
|
||||||
|
|
||||||
def text_table_strategy(strategy_results, stake_currency: str) -> str:
|
def text_table_strategy(strategy_results, stake_currency: str) -> str:
|
||||||
"""
|
"""
|
||||||
Generate summary table per strategy
|
Generate summary table per strategy
|
||||||
@ -557,19 +683,22 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
|
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"),
|
('Total profit %', f"{strat_results['profit_total']:.2%}"),
|
||||||
|
('Trades per day', strat_results['trades_per_day']),
|
||||||
|
('Avg. daily profit %',
|
||||||
|
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
|
||||||
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
|
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Total trade volume', round_coin_value(strat_results['total_volume'],
|
('Total trade volume', round_coin_value(strat_results['total_volume'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('', ''), # Empty line to improve readability
|
('', ''), # Empty line to improve readability
|
||||||
('Best Pair', f"{strat_results['best_pair']['key']} "
|
('Best Pair', f"{strat_results['best_pair']['key']} "
|
||||||
f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"),
|
f"{strat_results['best_pair']['profit_sum']:.2%}"),
|
||||||
('Worst Pair', f"{strat_results['worst_pair']['key']} "
|
('Worst Pair', f"{strat_results['worst_pair']['key']} "
|
||||||
f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"),
|
f"{strat_results['worst_pair']['profit_sum']:.2%}"),
|
||||||
('Best trade', f"{best_trade['pair']} {round(best_trade['profit_ratio'] * 100, 2)}%"),
|
('Best trade', f"{best_trade['pair']} {best_trade['profit_ratio']:.2%}"),
|
||||||
('Worst trade', f"{worst_trade['pair']} "
|
('Worst trade', f"{worst_trade['pair']} "
|
||||||
f"{round(worst_trade['profit_ratio'] * 100, 2)}%"),
|
f"{worst_trade['profit_ratio']:.2%}"),
|
||||||
|
|
||||||
('Best day', round_coin_value(strat_results['backtest_best_day_abs'],
|
('Best day', round_coin_value(strat_results['backtest_best_day_abs'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
@ -587,7 +716,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
('Max balance', round_coin_value(strat_results['csum_max'],
|
('Max balance', round_coin_value(strat_results['csum_max'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
|
|
||||||
('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"),
|
('Drawdown', f"{strat_results['max_drawdown']:.2%}"),
|
||||||
('Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
|
('Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
|
('Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
|
||||||
@ -596,7 +725,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Drawdown Start', strat_results['drawdown_start']),
|
('Drawdown Start', strat_results['drawdown_start']),
|
||||||
('Drawdown End', strat_results['drawdown_end']),
|
('Drawdown End', strat_results['drawdown_end']),
|
||||||
('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"),
|
('Market change', f"{strat_results['market_change']:.2%}"),
|
||||||
]
|
]
|
||||||
|
|
||||||
return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
|
return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
|
||||||
@ -614,7 +743,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
||||||
def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str):
|
def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str,
|
||||||
|
backtest_breakdown=[]):
|
||||||
"""
|
"""
|
||||||
Print results for one strategy
|
Print results for one strategy
|
||||||
"""
|
"""
|
||||||
@ -625,6 +755,16 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
|||||||
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
|
if results.get('results_per_buy_tag') is not None:
|
||||||
|
table = text_table_tags(
|
||||||
|
"buy_tag",
|
||||||
|
results['results_per_buy_tag'],
|
||||||
|
stake_currency=stake_currency)
|
||||||
|
|
||||||
|
if isinstance(table, str) and len(table) > 0:
|
||||||
|
print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '='))
|
||||||
|
print(table)
|
||||||
|
|
||||||
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
|
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
|
||||||
stake_currency=stake_currency)
|
stake_currency=stake_currency)
|
||||||
if isinstance(table, str) and len(table) > 0:
|
if isinstance(table, str) and len(table) > 0:
|
||||||
@ -636,6 +776,15 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
|||||||
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
|
for period in backtest_breakdown:
|
||||||
|
days_breakdown_stats = generate_periodic_breakdown_stats(
|
||||||
|
trade_list=results['trades'], period=period)
|
||||||
|
table = text_table_periodic_breakdown(days_breakdown_stats=days_breakdown_stats,
|
||||||
|
stake_currency=stake_currency, period=period)
|
||||||
|
if isinstance(table, str) and len(table) > 0:
|
||||||
|
print(f' {period.upper()} BREAKDOWN '.center(len(table.splitlines()[0]), '='))
|
||||||
|
print(table)
|
||||||
|
|
||||||
table = text_table_add_metrics(results)
|
table = text_table_add_metrics(results)
|
||||||
if isinstance(table, str) and len(table) > 0:
|
if isinstance(table, str) and len(table) > 0:
|
||||||
print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '='))
|
print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '='))
|
||||||
@ -643,6 +792,7 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
|||||||
|
|
||||||
if isinstance(table, str) and len(table) > 0:
|
if isinstance(table, str) and len(table) > 0:
|
||||||
print('=' * len(table.splitlines()[0]))
|
print('=' * len(table.splitlines()[0]))
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
@ -650,7 +800,9 @@ def show_backtest_results(config: Dict, backtest_stats: Dict):
|
|||||||
stake_currency = config['stake_currency']
|
stake_currency = config['stake_currency']
|
||||||
|
|
||||||
for strategy, results in backtest_stats['strategy'].items():
|
for strategy, results in backtest_stats['strategy'].items():
|
||||||
show_backtest_result(strategy, results, stake_currency)
|
show_backtest_result(
|
||||||
|
strategy, results, stake_currency,
|
||||||
|
config.get('backtest_breakdown', []))
|
||||||
|
|
||||||
if len(backtest_stats['strategy']) > 1:
|
if len(backtest_stats['strategy']) > 1:
|
||||||
# Print Strategy summary table
|
# Print Strategy summary table
|
||||||
@ -662,3 +814,13 @@ def show_backtest_results(config: Dict, backtest_stats: Dict):
|
|||||||
print(table)
|
print(table)
|
||||||
print('=' * len(table.splitlines()[0]))
|
print('=' * len(table.splitlines()[0]))
|
||||||
print('\nFor more details, please look at the detail tables above')
|
print('\nFor more details, please look at the detail tables above')
|
||||||
|
|
||||||
|
|
||||||
|
def show_sorted_pairlist(config: Dict, backtest_stats: Dict):
|
||||||
|
if config.get('backtest_show_pair_list', False):
|
||||||
|
for strategy, results in backtest_stats['strategy'].items():
|
||||||
|
print(f"Pairs for Strategy {strategy}: \n[")
|
||||||
|
for result in results['results_per_pair']:
|
||||||
|
if result["key"] != 'TOTAL':
|
||||||
|
print(f'"{result["key"]}", // {result["profit_mean"]:.2%}')
|
||||||
|
print("]")
|
||||||
|
@ -7,11 +7,15 @@ class SKDecimal(Integer):
|
|||||||
def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None,
|
def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None,
|
||||||
name=None, dtype=np.int64):
|
name=None, dtype=np.int64):
|
||||||
self.decimals = decimals
|
self.decimals = decimals
|
||||||
_low = int(low * pow(10, self.decimals))
|
|
||||||
_high = int(high * pow(10, self.decimals))
|
self.pow_dot_one = pow(0.1, self.decimals)
|
||||||
|
self.pow_ten = pow(10, self.decimals)
|
||||||
|
|
||||||
|
_low = int(low * self.pow_ten)
|
||||||
|
_high = int(high * self.pow_ten)
|
||||||
# trunc to precision to avoid points out of space
|
# trunc to precision to avoid points out of space
|
||||||
self.low_orig = round(_low * pow(0.1, self.decimals), self.decimals)
|
self.low_orig = round(_low * self.pow_dot_one, self.decimals)
|
||||||
self.high_orig = round(_high * pow(0.1, self.decimals), self.decimals)
|
self.high_orig = round(_high * self.pow_dot_one, self.decimals)
|
||||||
|
|
||||||
super().__init__(_low, _high, prior, base, transform, name, dtype)
|
super().__init__(_low, _high, prior, base, transform, name, dtype)
|
||||||
|
|
||||||
@ -25,9 +29,9 @@ class SKDecimal(Integer):
|
|||||||
return self.low_orig <= point <= self.high_orig
|
return self.low_orig <= point <= self.high_orig
|
||||||
|
|
||||||
def transform(self, Xt):
|
def transform(self, Xt):
|
||||||
aa = [int(x * pow(10, self.decimals)) for x in Xt]
|
return super().transform([int(v * self.pow_ten) for v in Xt])
|
||||||
return super().transform(aa)
|
|
||||||
|
|
||||||
def inverse_transform(self, Xt):
|
def inverse_transform(self, Xt):
|
||||||
res = super().inverse_transform(Xt)
|
res = super().inverse_transform(Xt)
|
||||||
return [round(x * pow(0.1, self.decimals), self.decimals) for x in res]
|
# equivalent to [round(x * pow(0.1, self.decimals), self.decimals) for x in res]
|
||||||
|
return [int(v) / self.pow_ten for v in res]
|
||||||
|
@ -195,6 +195,8 @@ class Order(_DECL_BASE):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_open_orders() -> List['Order']:
|
def get_open_orders() -> List['Order']:
|
||||||
"""
|
"""
|
||||||
|
Retrieve open orders from the database
|
||||||
|
:return: List of open orders
|
||||||
"""
|
"""
|
||||||
return Order.query.filter(Order.ft_is_open.is_(True)).all()
|
return Order.query.filter(Order.ft_is_open.is_(True)).all()
|
||||||
|
|
||||||
@ -491,6 +493,13 @@ class LocalTrade():
|
|||||||
def update_order(self, order: Dict) -> None:
|
def update_order(self, order: Dict) -> None:
|
||||||
Order.update_orders(self.orders, order)
|
Order.update_orders(self.orders, order)
|
||||||
|
|
||||||
|
def get_exit_order_count(self) -> int:
|
||||||
|
"""
|
||||||
|
Get amount of failed exiting orders
|
||||||
|
assumes full exits.
|
||||||
|
"""
|
||||||
|
return len([o for o in self.orders if o.ft_order_side == 'sell'])
|
||||||
|
|
||||||
def _calc_open_trade_value(self) -> float:
|
def _calc_open_trade_value(self) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the open_rate including open_fee.
|
Calculate the open_rate including open_fee.
|
||||||
@ -775,7 +784,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
return Trade.query
|
return Trade.query
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_open_order_trades():
|
def get_open_order_trades() -> List['Trade']:
|
||||||
"""
|
"""
|
||||||
Returns all open trades
|
Returns all open trades
|
||||||
NOTE: Not supported in Backtesting.
|
NOTE: Not supported in Backtesting.
|
||||||
@ -853,13 +862,132 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'profit': profit,
|
'profit_ratio': profit,
|
||||||
|
'profit': round(profit * 100, 2), # Compatibility mode
|
||||||
|
'profit_pct': round(profit * 100, 2),
|
||||||
'profit_abs': profit_abs,
|
'profit_abs': profit_abs,
|
||||||
'count': count
|
'count': count
|
||||||
}
|
}
|
||||||
for pair, profit, profit_abs, count in pair_rates
|
for pair, profit, profit_abs, count in pair_rates
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_buy_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Returns List of dicts containing all Trades, based on buy tag performance
|
||||||
|
Can either be average for all pairs or a specific pair provided
|
||||||
|
NOTE: Not supported in Backtesting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filters = [Trade.is_open.is_(False)]
|
||||||
|
if(pair is not None):
|
||||||
|
filters.append(Trade.pair == pair)
|
||||||
|
|
||||||
|
buy_tag_perf = Trade.query.with_entities(
|
||||||
|
Trade.buy_tag,
|
||||||
|
func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
|
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||||
|
func.count(Trade.pair).label('count')
|
||||||
|
).filter(*filters)\
|
||||||
|
.group_by(Trade.buy_tag) \
|
||||||
|
.order_by(desc('profit_sum_abs')) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'buy_tag': buy_tag if buy_tag is not None else "Other",
|
||||||
|
'profit_ratio': profit,
|
||||||
|
'profit_pct': round(profit * 100, 2),
|
||||||
|
'profit_abs': profit_abs,
|
||||||
|
'count': count
|
||||||
|
}
|
||||||
|
for buy_tag, profit, profit_abs, count in buy_tag_perf
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_sell_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Returns List of dicts containing all Trades, based on sell reason performance
|
||||||
|
Can either be average for all pairs or a specific pair provided
|
||||||
|
NOTE: Not supported in Backtesting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filters = [Trade.is_open.is_(False)]
|
||||||
|
if(pair is not None):
|
||||||
|
filters.append(Trade.pair == pair)
|
||||||
|
|
||||||
|
sell_tag_perf = Trade.query.with_entities(
|
||||||
|
Trade.sell_reason,
|
||||||
|
func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
|
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||||
|
func.count(Trade.pair).label('count')
|
||||||
|
).filter(*filters)\
|
||||||
|
.group_by(Trade.sell_reason) \
|
||||||
|
.order_by(desc('profit_sum_abs')) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'sell_reason': sell_reason if sell_reason is not None else "Other",
|
||||||
|
'profit_ratio': profit,
|
||||||
|
'profit_pct': round(profit * 100, 2),
|
||||||
|
'profit_abs': profit_abs,
|
||||||
|
'count': count
|
||||||
|
}
|
||||||
|
for sell_reason, profit, profit_abs, count in sell_tag_perf
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_mix_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Returns List of dicts containing all Trades, based on buy_tag + sell_reason performance
|
||||||
|
Can either be average for all pairs or a specific pair provided
|
||||||
|
NOTE: Not supported in Backtesting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filters = [Trade.is_open.is_(False)]
|
||||||
|
if(pair is not None):
|
||||||
|
filters.append(Trade.pair == pair)
|
||||||
|
|
||||||
|
mix_tag_perf = Trade.query.with_entities(
|
||||||
|
Trade.id,
|
||||||
|
Trade.buy_tag,
|
||||||
|
Trade.sell_reason,
|
||||||
|
func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
|
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||||
|
func.count(Trade.pair).label('count')
|
||||||
|
).filter(*filters)\
|
||||||
|
.group_by(Trade.id) \
|
||||||
|
.order_by(desc('profit_sum_abs')) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
return_list: List[Dict] = []
|
||||||
|
for id, buy_tag, sell_reason, profit, profit_abs, count in mix_tag_perf:
|
||||||
|
buy_tag = buy_tag if buy_tag is not None else "Other"
|
||||||
|
sell_reason = sell_reason if sell_reason is not None else "Other"
|
||||||
|
|
||||||
|
if(sell_reason is not None and buy_tag is not None):
|
||||||
|
mix_tag = buy_tag + " " + sell_reason
|
||||||
|
i = 0
|
||||||
|
if not any(item["mix_tag"] == mix_tag for item in return_list):
|
||||||
|
return_list.append({'mix_tag': mix_tag,
|
||||||
|
'profit': profit,
|
||||||
|
'profit_pct': round(profit * 100, 2),
|
||||||
|
'profit_abs': profit_abs,
|
||||||
|
'count': count})
|
||||||
|
else:
|
||||||
|
while i < len(return_list):
|
||||||
|
if return_list[i]["mix_tag"] == mix_tag:
|
||||||
|
return_list[i] = {
|
||||||
|
'mix_tag': mix_tag,
|
||||||
|
'profit': profit + return_list[i]["profit"],
|
||||||
|
'profit_pct': round(profit + return_list[i]["profit"] * 100, 2),
|
||||||
|
'profit_abs': profit_abs + return_list[i]["profit_abs"],
|
||||||
|
'count': 1 + return_list[i]["count"]}
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return return_list
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
|
def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
|
||||||
"""
|
"""
|
||||||
@ -896,7 +1024,7 @@ class PairLock(_DECL_BASE):
|
|||||||
lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT)
|
lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT)
|
||||||
lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT)
|
lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT)
|
||||||
return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, '
|
return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, '
|
||||||
f'lock_end_time={lock_end_time})')
|
f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def query_pair_locks(pair: Optional[str], now: datetime) -> Query:
|
def query_pair_locks(pair: Optional[str], now: datetime) -> Query:
|
||||||
@ -905,7 +1033,6 @@ class PairLock(_DECL_BASE):
|
|||||||
:param pair: Pair to check for. Returns all current locks if pair is empty
|
:param pair: Pair to check for. Returns all current locks if pair is empty
|
||||||
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
filters = [PairLock.lock_end_time > now,
|
filters = [PairLock.lock_end_time > now,
|
||||||
# Only active locks
|
# Only active locks
|
||||||
PairLock.active.is_(True), ]
|
PairLock.active.is_(True), ]
|
||||||
|
@ -103,6 +103,36 @@ class PairLocks():
|
|||||||
if PairLocks.use_db:
|
if PairLocks.use_db:
|
||||||
PairLock.query.session.commit()
|
PairLock.query.session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def unlock_reason(reason: str, now: Optional[datetime] = None) -> None:
|
||||||
|
"""
|
||||||
|
Release all locks for this reason.
|
||||||
|
:param reason: Which reason to unlock
|
||||||
|
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||||
|
defaults to datetime.now(timezone.utc)
|
||||||
|
"""
|
||||||
|
if not now:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
if PairLocks.use_db:
|
||||||
|
# used in live modes
|
||||||
|
logger.info(f"Releasing all locks with reason '{reason}':")
|
||||||
|
filters = [PairLock.lock_end_time > now,
|
||||||
|
PairLock.active.is_(True),
|
||||||
|
PairLock.reason == reason
|
||||||
|
]
|
||||||
|
locks = PairLock.query.filter(*filters)
|
||||||
|
for lock in locks:
|
||||||
|
logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.")
|
||||||
|
lock.active = False
|
||||||
|
PairLock.query.session.commit()
|
||||||
|
else:
|
||||||
|
# used in backtesting mode; don't show log messages for speed
|
||||||
|
locks = PairLocks.get_pair_locks(None)
|
||||||
|
for lock in locks:
|
||||||
|
if lock.reason == reason:
|
||||||
|
lock.active = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_global_lock(now: Optional[datetime] = None) -> bool:
|
def is_global_lock(now: Optional[datetime] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -128,7 +158,9 @@ class PairLocks():
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all_locks() -> List[PairLock]:
|
def get_all_locks() -> List[PairLock]:
|
||||||
|
"""
|
||||||
|
Return all locks, also locks with expired end date
|
||||||
|
"""
|
||||||
if PairLocks.use_db:
|
if PairLocks.use_db:
|
||||||
return PairLock.query.all()
|
return PairLock.query.all()
|
||||||
else:
|
else:
|
||||||
|
@ -169,8 +169,8 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
|
|||||||
df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), 'cum_profit'],
|
df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), 'cum_profit'],
|
||||||
],
|
],
|
||||||
mode='markers',
|
mode='markers',
|
||||||
name=f"Max drawdown {max_drawdown * 100:.2f}%",
|
name=f"Max drawdown {max_drawdown:.2%}",
|
||||||
text=f"Max drawdown {max_drawdown * 100:.2f}%",
|
text=f"Max drawdown {max_drawdown:.2%}",
|
||||||
marker=dict(
|
marker=dict(
|
||||||
symbol='square-open',
|
symbol='square-open',
|
||||||
size=9,
|
size=9,
|
||||||
@ -192,7 +192,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
# Trades can be empty
|
# Trades can be empty
|
||||||
if trades is not None and len(trades) > 0:
|
if trades is not None and len(trades) > 0:
|
||||||
# Create description for sell summarizing the trade
|
# Create description for sell summarizing the trade
|
||||||
trades['desc'] = trades.apply(lambda row: f"{round(row['profit_ratio'] * 100, 1)}%, "
|
trades['desc'] = trades.apply(lambda row: f"{row['profit_ratio']:.2%}, "
|
||||||
f"{row['sell_reason']}, "
|
f"{row['sell_reason']}, "
|
||||||
f"{row['trade_duration']} min",
|
f"{row['trade_duration']} min",
|
||||||
axis=1)
|
axis=1)
|
||||||
|
@ -50,7 +50,7 @@ class PriceFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
active_price_filters = []
|
active_price_filters = []
|
||||||
if self._low_price_ratio != 0:
|
if self._low_price_ratio != 0:
|
||||||
active_price_filters.append(f"below {self._low_price_ratio * 100}%")
|
active_price_filters.append(f"below {self._low_price_ratio:.1%}")
|
||||||
if self._min_price != 0:
|
if self._min_price != 0:
|
||||||
active_price_filters.append(f"below {self._min_price:.8f}")
|
active_price_filters.append(f"below {self._min_price:.8f}")
|
||||||
if self._max_price != 0:
|
if self._max_price != 0:
|
||||||
@ -82,7 +82,7 @@ class PriceFilter(IPairList):
|
|||||||
changeperc = compare / ticker['last']
|
changeperc = compare / ticker['last']
|
||||||
if changeperc > self._low_price_ratio:
|
if changeperc > self._low_price_ratio:
|
||||||
self.log_once(f"Removed {pair} from whitelist, "
|
self.log_once(f"Removed {pair} from whitelist, "
|
||||||
f"because 1 unit is {changeperc * 100:.3f}%", logger.info)
|
f"because 1 unit is {changeperc:.3%}", logger.info)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Perform low_amount check
|
# Perform low_amount check
|
||||||
|
@ -5,6 +5,7 @@ import logging
|
|||||||
import random
|
import random
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from freqtrade.enums.runmode import RunMode
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -18,7 +19,15 @@ class ShuffleFilter(IPairList):
|
|||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
self._seed = pairlistconfig.get('seed')
|
# Apply seed in backtesting mode to get comparable results,
|
||||||
|
# but not in live modes to get a non-repeating order of pairs during live modes.
|
||||||
|
if config.get('runmode') in (RunMode.LIVE, RunMode.DRY_RUN):
|
||||||
|
self._seed = None
|
||||||
|
logger.info("Live mode detected, not applying seed.")
|
||||||
|
else:
|
||||||
|
self._seed = pairlistconfig.get('seed')
|
||||||
|
logger.info(f"Backtesting mode detected, applying seed value: {self._seed}")
|
||||||
|
|
||||||
self._random = random.Random(self._seed)
|
self._random = random.Random(self._seed)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -34,7 +34,7 @@ class SpreadFilter(IPairList):
|
|||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
"""
|
"""
|
||||||
return (f"{self.name} - Filtering pairs with ask/bid diff above "
|
return (f"{self.name} - Filtering pairs with ask/bid diff above "
|
||||||
f"{self._max_spread_ratio * 100}%.")
|
f"{self._max_spread_ratio:.2%}.")
|
||||||
|
|
||||||
def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
|
def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -47,7 +47,7 @@ class SpreadFilter(IPairList):
|
|||||||
spread = 1 - ticker['bid'] / ticker['ask']
|
spread = 1 - ticker['bid'] / ticker['ask']
|
||||||
if spread > self._max_spread_ratio:
|
if spread > self._max_spread_ratio:
|
||||||
self.log_once(f"Removed {pair} from whitelist, because spread "
|
self.log_once(f"Removed {pair} from whitelist, because spread "
|
||||||
f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%",
|
f"{spread * 100:.3%} > {self._max_spread_ratio:.3%}",
|
||||||
logger.info)
|
logger.info)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
@ -4,9 +4,9 @@ Static Pair List provider
|
|||||||
Provides pair white list as it configured in config
|
Provides pair white list as it configured in config
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -20,10 +20,6 @@ class StaticPairList(IPairList):
|
|||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
if self._pairlist_pos != 0:
|
|
||||||
raise OperationalException(f"{self.name} can only be used in the first position "
|
|
||||||
"in the list of Pairlist Handlers.")
|
|
||||||
|
|
||||||
self._allow_inactive = self._pairlistconfig.get('allow_inactive', False)
|
self._allow_inactive = self._pairlistconfig.get('allow_inactive', False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -64,4 +60,8 @@ class StaticPairList(IPairList):
|
|||||||
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
||||||
:return: new whitelist
|
:return: new whitelist
|
||||||
"""
|
"""
|
||||||
return pairlist
|
pairlist_ = deepcopy(pairlist)
|
||||||
|
for pair in self._config['exchange']['pair_whitelist']:
|
||||||
|
if pair not in pairlist_:
|
||||||
|
pairlist_.append(pair)
|
||||||
|
return pairlist_
|
||||||
|
@ -91,7 +91,7 @@ class IResolver:
|
|||||||
logger.debug(f"Searching for {cls.object_type.__name__} {object_name} in '{directory}'")
|
logger.debug(f"Searching for {cls.object_type.__name__} {object_name} in '{directory}'")
|
||||||
for entry in directory.iterdir():
|
for entry in directory.iterdir():
|
||||||
# Only consider python files
|
# Only consider python files
|
||||||
if not str(entry).endswith('.py'):
|
if entry.suffix != '.py':
|
||||||
logger.debug('Ignoring %s', entry)
|
logger.debug('Ignoring %s', entry)
|
||||||
continue
|
continue
|
||||||
if entry.is_symlink() and not entry.is_file():
|
if entry.is_symlink() and not entry.is_file():
|
||||||
@ -169,7 +169,7 @@ class IResolver:
|
|||||||
objects = []
|
objects = []
|
||||||
for entry in directory.iterdir():
|
for entry in directory.iterdir():
|
||||||
# Only consider python files
|
# Only consider python files
|
||||||
if not str(entry).endswith('.py'):
|
if entry.suffix != '.py':
|
||||||
logger.debug('Ignoring %s', entry)
|
logger.debug('Ignoring %s', entry)
|
||||||
continue
|
continue
|
||||||
module_path = entry.resolve()
|
module_path = entry.resolve()
|
||||||
|
@ -56,17 +56,21 @@ class StrategyResolver(IResolver):
|
|||||||
if strategy._ft_params_from_file:
|
if strategy._ft_params_from_file:
|
||||||
# Set parameters from Hyperopt results file
|
# Set parameters from Hyperopt results file
|
||||||
params = strategy._ft_params_from_file
|
params = strategy._ft_params_from_file
|
||||||
strategy.minimal_roi = params.get('roi', strategy.minimal_roi)
|
strategy.minimal_roi = params.get('roi', getattr(strategy, 'minimal_roi', {}))
|
||||||
|
|
||||||
strategy.stoploss = params.get('stoploss', {}).get('stoploss', strategy.stoploss)
|
strategy.stoploss = params.get('stoploss', {}).get(
|
||||||
|
'stoploss', getattr(strategy, 'stoploss', -0.1))
|
||||||
trailing = params.get('trailing', {})
|
trailing = params.get('trailing', {})
|
||||||
strategy.trailing_stop = trailing.get('trailing_stop', strategy.trailing_stop)
|
strategy.trailing_stop = trailing.get(
|
||||||
strategy.trailing_stop_positive = trailing.get('trailing_stop_positive',
|
'trailing_stop', getattr(strategy, 'trailing_stop', False))
|
||||||
strategy.trailing_stop_positive)
|
strategy.trailing_stop_positive = trailing.get(
|
||||||
|
'trailing_stop_positive', getattr(strategy, 'trailing_stop_positive', None))
|
||||||
strategy.trailing_stop_positive_offset = trailing.get(
|
strategy.trailing_stop_positive_offset = trailing.get(
|
||||||
'trailing_stop_positive_offset', strategy.trailing_stop_positive_offset)
|
'trailing_stop_positive_offset',
|
||||||
|
getattr(strategy, 'trailing_stop_positive_offset', 0))
|
||||||
strategy.trailing_only_offset_is_reached = trailing.get(
|
strategy.trailing_only_offset_is_reached = trailing.get(
|
||||||
'trailing_only_offset_is_reached', strategy.trailing_only_offset_is_reached)
|
'trailing_only_offset_is_reached',
|
||||||
|
getattr(strategy, 'trailing_only_offset_is_reached', 0.0))
|
||||||
|
|
||||||
# Set attributes
|
# Set attributes
|
||||||
# Check if we need to override configuration
|
# Check if we need to override configuration
|
||||||
|
@ -4,6 +4,7 @@ from typing import Any, Dict, List, Optional, Union
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
|
from freqtrade.enums import OrderTypeValues
|
||||||
|
|
||||||
|
|
||||||
class Ping(BaseModel):
|
class Ping(BaseModel):
|
||||||
@ -63,6 +64,8 @@ class Count(BaseModel):
|
|||||||
class PerformanceEntry(BaseModel):
|
class PerformanceEntry(BaseModel):
|
||||||
pair: str
|
pair: str
|
||||||
profit: float
|
profit: float
|
||||||
|
profit_ratio: float
|
||||||
|
profit_pct: float
|
||||||
profit_abs: float
|
profit_abs: float
|
||||||
count: int
|
count: int
|
||||||
|
|
||||||
@ -93,6 +96,7 @@ class Profit(BaseModel):
|
|||||||
avg_duration: str
|
avg_duration: str
|
||||||
best_pair: str
|
best_pair: str
|
||||||
best_rate: float
|
best_rate: float
|
||||||
|
best_pair_profit_ratio: float
|
||||||
winning_trades: int
|
winning_trades: int
|
||||||
losing_trades: int
|
losing_trades: int
|
||||||
|
|
||||||
@ -121,7 +125,27 @@ class Daily(BaseModel):
|
|||||||
stake_currency: str
|
stake_currency: str
|
||||||
|
|
||||||
|
|
||||||
|
class UnfilledTimeout(BaseModel):
|
||||||
|
buy: Optional[int]
|
||||||
|
sell: Optional[int]
|
||||||
|
unit: Optional[str]
|
||||||
|
exit_timeout_count: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
class OrderTypes(BaseModel):
|
||||||
|
buy: OrderTypeValues
|
||||||
|
sell: OrderTypeValues
|
||||||
|
emergencysell: Optional[OrderTypeValues]
|
||||||
|
forcesell: Optional[OrderTypeValues]
|
||||||
|
forcebuy: Optional[OrderTypeValues]
|
||||||
|
stoploss: OrderTypeValues
|
||||||
|
stoploss_on_exchange: bool
|
||||||
|
stoploss_on_exchange_interval: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class ShowConfig(BaseModel):
|
class ShowConfig(BaseModel):
|
||||||
|
version: str
|
||||||
|
api_version: float
|
||||||
dry_run: bool
|
dry_run: bool
|
||||||
stake_currency: str
|
stake_currency: str
|
||||||
stake_amount: Union[float, str]
|
stake_amount: Union[float, str]
|
||||||
@ -134,6 +158,8 @@ class ShowConfig(BaseModel):
|
|||||||
trailing_stop_positive: Optional[float]
|
trailing_stop_positive: Optional[float]
|
||||||
trailing_stop_positive_offset: Optional[float]
|
trailing_stop_positive_offset: Optional[float]
|
||||||
trailing_only_offset_is_reached: Optional[bool]
|
trailing_only_offset_is_reached: Optional[bool]
|
||||||
|
unfilledtimeout: UnfilledTimeout
|
||||||
|
order_types: OrderTypes
|
||||||
use_custom_stoploss: Optional[bool]
|
use_custom_stoploss: Optional[bool]
|
||||||
timeframe: Optional[str]
|
timeframe: Optional[str]
|
||||||
timeframe_ms: int
|
timeframe_ms: int
|
||||||
@ -249,10 +275,12 @@ class Logs(BaseModel):
|
|||||||
class ForceBuyPayload(BaseModel):
|
class ForceBuyPayload(BaseModel):
|
||||||
pair: str
|
pair: str
|
||||||
price: Optional[float]
|
price: Optional[float]
|
||||||
|
ordertype: Optional[OrderTypeValues]
|
||||||
|
|
||||||
|
|
||||||
class ForceSellPayload(BaseModel):
|
class ForceSellPayload(BaseModel):
|
||||||
tradeid: str
|
tradeid: str
|
||||||
|
ordertype: Optional[OrderTypeValues]
|
||||||
|
|
||||||
|
|
||||||
class BlacklistPayload(BaseModel):
|
class BlacklistPayload(BaseModel):
|
||||||
|
@ -26,6 +26,12 @@ from freqtrade.rpc.rpc import RPCException
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# API version
|
||||||
|
# Pre-1.1, no version was provided
|
||||||
|
# Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen.
|
||||||
|
# 1.11: forcebuy and forcesell accept ordertype
|
||||||
|
API_VERSION = 1.11
|
||||||
|
|
||||||
# Public API, requires no auth.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
# Private API, protected by authentication
|
# Private API, protected by authentication
|
||||||
@ -117,12 +123,15 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
|
|||||||
state = ''
|
state = ''
|
||||||
if rpc:
|
if rpc:
|
||||||
state = rpc._freqtrade.state
|
state = rpc._freqtrade.state
|
||||||
return RPC._rpc_show_config(config, state)
|
resp = RPC._rpc_show_config(config, state)
|
||||||
|
resp['api_version'] = API_VERSION
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
|
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
|
||||||
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
||||||
trade = rpc._rpc_forcebuy(payload.pair, payload.price)
|
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||||
|
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype)
|
||||||
|
|
||||||
if trade:
|
if trade:
|
||||||
return ForceBuyResponse.parse_obj(trade.to_json())
|
return ForceBuyResponse.parse_obj(trade.to_json())
|
||||||
@ -132,7 +141,8 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
|||||||
|
|
||||||
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
|
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
|
||||||
def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)):
|
def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)):
|
||||||
return rpc._rpc_forcesell(payload.tradeid)
|
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||||
|
return rpc._rpc_forcesell(payload.tradeid, ordertype)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
|
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
|
||||||
|
@ -9,9 +9,11 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import psutil
|
import psutil
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
from numpy import NAN, inf, int64, mean
|
from numpy import NAN, inf, int64, mean
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import __version__
|
||||||
from freqtrade.configuration.timerange import TimeRange
|
from freqtrade.configuration.timerange import TimeRange
|
||||||
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
|
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.data.history import load_data
|
from freqtrade.data.history import load_data
|
||||||
@ -103,9 +105,10 @@ class RPC:
|
|||||||
information via rpc.
|
information via rpc.
|
||||||
"""
|
"""
|
||||||
val = {
|
val = {
|
||||||
|
'version': __version__,
|
||||||
'dry_run': config['dry_run'],
|
'dry_run': config['dry_run'],
|
||||||
'stake_currency': config['stake_currency'],
|
'stake_currency': config['stake_currency'],
|
||||||
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
||||||
'stake_amount': config['stake_amount'],
|
'stake_amount': config['stake_amount'],
|
||||||
'available_capital': config.get('available_capital'),
|
'available_capital': config.get('available_capital'),
|
||||||
'max_open_trades': (config['max_open_trades']
|
'max_open_trades': (config['max_open_trades']
|
||||||
@ -116,7 +119,9 @@ class RPC:
|
|||||||
'trailing_stop_positive': config.get('trailing_stop_positive'),
|
'trailing_stop_positive': config.get('trailing_stop_positive'),
|
||||||
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
|
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
|
||||||
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
|
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
|
||||||
|
'unfilledtimeout': config.get('unfilledtimeout'),
|
||||||
'use_custom_stoploss': config.get('use_custom_stoploss'),
|
'use_custom_stoploss': config.get('use_custom_stoploss'),
|
||||||
|
'order_types': config.get('order_types'),
|
||||||
'bot_name': config.get('bot_name', 'freqtrade'),
|
'bot_name': config.get('bot_name', 'freqtrade'),
|
||||||
'timeframe': config.get('timeframe'),
|
'timeframe': config.get('timeframe'),
|
||||||
'timeframe_ms': timeframe_to_msecs(config['timeframe']
|
'timeframe_ms': timeframe_to_msecs(config['timeframe']
|
||||||
@ -219,9 +224,8 @@ class RPC:
|
|||||||
trade.pair, refresh=False, side="sell")
|
trade.pair, refresh=False, side="sell")
|
||||||
except (PricingError, ExchangeError):
|
except (PricingError, ExchangeError):
|
||||||
current_rate = NAN
|
current_rate = NAN
|
||||||
trade_percent = (100 * trade.calc_profit_ratio(current_rate))
|
|
||||||
trade_profit = trade.calc_profit(current_rate)
|
trade_profit = trade.calc_profit(current_rate)
|
||||||
profit_str = f'{trade_percent:.2f}%'
|
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
|
||||||
if self._fiat_converter:
|
if self._fiat_converter:
|
||||||
fiat_profit = self._fiat_converter.convert_amount(
|
fiat_profit = self._fiat_converter.convert_amount(
|
||||||
trade_profit,
|
trade_profit,
|
||||||
@ -250,7 +254,7 @@ class RPC:
|
|||||||
def _rpc_daily_profit(
|
def _rpc_daily_profit(
|
||||||
self, timescale: int,
|
self, timescale: int,
|
||||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||||
today = datetime.utcnow().date()
|
today = datetime.now(timezone.utc).date()
|
||||||
profit_days: Dict[date, Dict] = {}
|
profit_days: Dict[date, Dict] = {}
|
||||||
|
|
||||||
if not (isinstance(timescale, int) and timescale > 0):
|
if not (isinstance(timescale, int) and timescale > 0):
|
||||||
@ -289,6 +293,91 @@ class RPC:
|
|||||||
'data': data
|
'data': data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _rpc_weekly_profit(
|
||||||
|
self, timescale: int,
|
||||||
|
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||||
|
today = datetime.now(timezone.utc).date()
|
||||||
|
first_iso_day_of_week = today - timedelta(days=today.weekday()) # Monday
|
||||||
|
profit_weeks: Dict[date, Dict] = {}
|
||||||
|
|
||||||
|
if not (isinstance(timescale, int) and timescale > 0):
|
||||||
|
raise RPCException('timescale must be an integer greater than 0')
|
||||||
|
|
||||||
|
for week in range(0, timescale):
|
||||||
|
profitweek = first_iso_day_of_week - timedelta(weeks=week)
|
||||||
|
trades = Trade.get_trades(trade_filter=[
|
||||||
|
Trade.is_open.is_(False),
|
||||||
|
Trade.close_date >= profitweek,
|
||||||
|
Trade.close_date < (profitweek + timedelta(weeks=1))
|
||||||
|
]).order_by(Trade.close_date).all()
|
||||||
|
curweekprofit = sum(
|
||||||
|
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
||||||
|
profit_weeks[profitweek] = {
|
||||||
|
'amount': curweekprofit,
|
||||||
|
'trades': len(trades)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
'date': key,
|
||||||
|
'abs_profit': value["amount"],
|
||||||
|
'fiat_value': self._fiat_converter.convert_amount(
|
||||||
|
value['amount'],
|
||||||
|
stake_currency,
|
||||||
|
fiat_display_currency
|
||||||
|
) if self._fiat_converter else 0,
|
||||||
|
'trade_count': value["trades"],
|
||||||
|
}
|
||||||
|
for key, value in profit_weeks.items()
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
'stake_currency': stake_currency,
|
||||||
|
'fiat_display_currency': fiat_display_currency,
|
||||||
|
'data': data
|
||||||
|
}
|
||||||
|
|
||||||
|
def _rpc_monthly_profit(
|
||||||
|
self, timescale: int,
|
||||||
|
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||||
|
first_day_of_month = datetime.now(timezone.utc).date().replace(day=1)
|
||||||
|
profit_months: Dict[date, Dict] = {}
|
||||||
|
|
||||||
|
if not (isinstance(timescale, int) and timescale > 0):
|
||||||
|
raise RPCException('timescale must be an integer greater than 0')
|
||||||
|
|
||||||
|
for month in range(0, timescale):
|
||||||
|
profitmonth = first_day_of_month - relativedelta(months=month)
|
||||||
|
trades = Trade.get_trades(trade_filter=[
|
||||||
|
Trade.is_open.is_(False),
|
||||||
|
Trade.close_date >= profitmonth,
|
||||||
|
Trade.close_date < (profitmonth + relativedelta(months=1))
|
||||||
|
]).order_by(Trade.close_date).all()
|
||||||
|
curmonthprofit = sum(
|
||||||
|
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
||||||
|
profit_months[profitmonth] = {
|
||||||
|
'amount': curmonthprofit,
|
||||||
|
'trades': len(trades)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
'date': f"{key.year}-{key.month:02d}",
|
||||||
|
'abs_profit': value["amount"],
|
||||||
|
'fiat_value': self._fiat_converter.convert_amount(
|
||||||
|
value['amount'],
|
||||||
|
stake_currency,
|
||||||
|
fiat_display_currency
|
||||||
|
) if self._fiat_converter else 0,
|
||||||
|
'trade_count': value["trades"],
|
||||||
|
}
|
||||||
|
for key, value in profit_months.items()
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
'stake_currency': stake_currency,
|
||||||
|
'fiat_display_currency': fiat_display_currency,
|
||||||
|
'data': data
|
||||||
|
}
|
||||||
|
|
||||||
def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict:
|
def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict:
|
||||||
""" Returns the X last trades """
|
""" Returns the X last trades """
|
||||||
order_by = Trade.id if order_by_id else Trade.close_date.desc()
|
order_by = Trade.id if order_by_id else Trade.close_date.desc()
|
||||||
@ -444,7 +533,8 @@ class RPC:
|
|||||||
'latest_trade_timestamp': int(last_date.timestamp() * 1000) if last_date else 0,
|
'latest_trade_timestamp': int(last_date.timestamp() * 1000) if last_date else 0,
|
||||||
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
|
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
|
||||||
'best_pair': best_pair[0] if best_pair else '',
|
'best_pair': best_pair[0] if best_pair else '',
|
||||||
'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0,
|
'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0, # Deprecated
|
||||||
|
'best_pair_profit_ratio': best_pair[1] if best_pair else 0,
|
||||||
'winning_trades': winning_trades,
|
'winning_trades': winning_trades,
|
||||||
'losing_trades': losing_trades,
|
'losing_trades': losing_trades,
|
||||||
}
|
}
|
||||||
@ -550,7 +640,7 @@ class RPC:
|
|||||||
|
|
||||||
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
|
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
|
||||||
|
|
||||||
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
|
def _rpc_forcesell(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Handler for forcesell <id>.
|
Handler for forcesell <id>.
|
||||||
Sells the given trade at current price
|
Sells the given trade at current price
|
||||||
@ -574,7 +664,11 @@ class RPC:
|
|||||||
current_rate = self._freqtrade.exchange.get_rate(
|
current_rate = self._freqtrade.exchange.get_rate(
|
||||||
trade.pair, refresh=False, side="sell")
|
trade.pair, refresh=False, side="sell")
|
||||||
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
||||||
self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason)
|
order_type = ordertype or self._freqtrade.strategy.order_types.get(
|
||||||
|
"forcesell", self._freqtrade.strategy.order_types["sell"])
|
||||||
|
|
||||||
|
self._freqtrade.execute_trade_exit(
|
||||||
|
trade, current_rate, sell_reason, ordertype=order_type)
|
||||||
# ---- EOF def _exec_forcesell ----
|
# ---- EOF def _exec_forcesell ----
|
||||||
|
|
||||||
if self._freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
@ -602,7 +696,8 @@ class RPC:
|
|||||||
self._freqtrade.wallets.update()
|
self._freqtrade.wallets.update()
|
||||||
return {'result': f'Created sell order for trade {trade_id}.'}
|
return {'result': f'Created sell order for trade {trade_id}.'}
|
||||||
|
|
||||||
def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]:
|
def _rpc_forcebuy(self, pair: str, price: Optional[float],
|
||||||
|
order_type: Optional[str] = None) -> Optional[Trade]:
|
||||||
"""
|
"""
|
||||||
Handler for forcebuy <asset> <price>
|
Handler for forcebuy <asset> <price>
|
||||||
Buys a pair trade at the given or current price
|
Buys a pair trade at the given or current price
|
||||||
@ -630,7 +725,10 @@ class RPC:
|
|||||||
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
|
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
|
||||||
|
|
||||||
# execute buy
|
# execute buy
|
||||||
if self._freqtrade.execute_entry(pair, stakeamount, price, forcebuy=True):
|
if not order_type:
|
||||||
|
order_type = self._freqtrade.strategy.order_types.get(
|
||||||
|
'forcebuy', self._freqtrade.strategy.order_types['buy'])
|
||||||
|
if self._freqtrade.execute_entry(pair, stakeamount, price, ordertype=order_type):
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||||
return trade
|
return trade
|
||||||
@ -682,10 +780,36 @@ class RPC:
|
|||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
"""
|
"""
|
||||||
pair_rates = Trade.get_overall_performance()
|
pair_rates = Trade.get_overall_performance()
|
||||||
# Round and convert to %
|
|
||||||
[x.update({'profit': round(x['profit'] * 100, 2)}) for x in pair_rates]
|
|
||||||
return pair_rates
|
return pair_rates
|
||||||
|
|
||||||
|
def _rpc_buy_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Handler for buy tag performance.
|
||||||
|
Shows a performance statistic from finished trades
|
||||||
|
"""
|
||||||
|
buy_tags = Trade.get_buy_tag_performance(pair)
|
||||||
|
|
||||||
|
return buy_tags
|
||||||
|
|
||||||
|
def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Handler for sell reason performance.
|
||||||
|
Shows a performance statistic from finished trades
|
||||||
|
"""
|
||||||
|
sell_reasons = Trade.get_sell_reason_performance(pair)
|
||||||
|
|
||||||
|
return sell_reasons
|
||||||
|
|
||||||
|
def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Handler for mix tag (buy_tag + sell_reason) performance.
|
||||||
|
Shows a performance statistic from finished trades
|
||||||
|
"""
|
||||||
|
mix_tags = Trade.get_mix_tag_performance(pair)
|
||||||
|
|
||||||
|
return mix_tags
|
||||||
|
|
||||||
def _rpc_count(self) -> Dict[str, float]:
|
def _rpc_count(self) -> Dict[str, float]:
|
||||||
""" Returns the number of trades running """
|
""" Returns the number of trades running """
|
||||||
if self._freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
@ -793,15 +917,15 @@ class RPC:
|
|||||||
if has_content:
|
if has_content:
|
||||||
|
|
||||||
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
|
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
|
||||||
# Move open to separate column when signal for easy plotting
|
# Move signal close to separate column when signal for easy plotting
|
||||||
if 'buy' in dataframe.columns:
|
if 'buy' in dataframe.columns:
|
||||||
buy_mask = (dataframe['buy'] == 1)
|
buy_mask = (dataframe['buy'] == 1)
|
||||||
buy_signals = int(buy_mask.sum())
|
buy_signals = int(buy_mask.sum())
|
||||||
dataframe.loc[buy_mask, '_buy_signal_open'] = dataframe.loc[buy_mask, 'open']
|
dataframe.loc[buy_mask, '_buy_signal_close'] = dataframe.loc[buy_mask, 'close']
|
||||||
if 'sell' in dataframe.columns:
|
if 'sell' in dataframe.columns:
|
||||||
sell_mask = (dataframe['sell'] == 1)
|
sell_mask = (dataframe['sell'] == 1)
|
||||||
sell_signals = int(sell_mask.sum())
|
sell_signals = int(sell_mask.sum())
|
||||||
dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open']
|
dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close']
|
||||||
dataframe = dataframe.replace([inf, -inf], NAN)
|
dataframe = dataframe.replace([inf, -inf], NAN)
|
||||||
dataframe = dataframe.replace({NAN: None})
|
dataframe = dataframe.replace({NAN: None})
|
||||||
|
|
||||||
|
@ -107,11 +107,12 @@ class Telegram(RPCHandler):
|
|||||||
# this needs refactoring of the whole telegram module (same
|
# this needs refactoring of the whole telegram module (same
|
||||||
# problem in _help()).
|
# problem in _help()).
|
||||||
valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$',
|
valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$',
|
||||||
r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$',
|
r'/trades$', r'/performance$', r'/buys', r'/sells', r'/mix_tags',
|
||||||
r'/profit$', r'/profit \d+',
|
r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+',
|
||||||
r'/stats$', r'/count$', r'/locks$', r'/balance$',
|
r'/stats$', r'/count$', r'/locks$', r'/balance$',
|
||||||
r'/stopbuy$', r'/reload_config$', r'/show_config$',
|
r'/stopbuy$', r'/reload_config$', r'/show_config$',
|
||||||
r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$',
|
r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$',
|
||||||
|
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
|
||||||
r'/forcebuy$', r'/help$', r'/version$']
|
r'/forcebuy$', r'/help$', r'/version$']
|
||||||
# Create keys for generation
|
# Create keys for generation
|
||||||
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
||||||
@ -154,8 +155,13 @@ class Telegram(RPCHandler):
|
|||||||
CommandHandler('trades', self._trades),
|
CommandHandler('trades', self._trades),
|
||||||
CommandHandler('delete', self._delete_trade),
|
CommandHandler('delete', self._delete_trade),
|
||||||
CommandHandler('performance', self._performance),
|
CommandHandler('performance', self._performance),
|
||||||
|
CommandHandler('buys', self._buy_tag_performance),
|
||||||
|
CommandHandler('sells', self._sell_reason_performance),
|
||||||
|
CommandHandler('mix_tags', self._mix_tag_performance),
|
||||||
CommandHandler('stats', self._stats),
|
CommandHandler('stats', self._stats),
|
||||||
CommandHandler('daily', self._daily),
|
CommandHandler('daily', self._daily),
|
||||||
|
CommandHandler('weekly', self._weekly),
|
||||||
|
CommandHandler('monthly', self._monthly),
|
||||||
CommandHandler('count', self._count),
|
CommandHandler('count', self._count),
|
||||||
CommandHandler('locks', self._locks),
|
CommandHandler('locks', self._locks),
|
||||||
CommandHandler(['unlock', 'delete_locks'], self._delete_locks),
|
CommandHandler(['unlock', 'delete_locks'], self._delete_locks),
|
||||||
@ -172,9 +178,15 @@ class Telegram(RPCHandler):
|
|||||||
callbacks = [
|
callbacks = [
|
||||||
CallbackQueryHandler(self._status_table, pattern='update_status_table'),
|
CallbackQueryHandler(self._status_table, pattern='update_status_table'),
|
||||||
CallbackQueryHandler(self._daily, pattern='update_daily'),
|
CallbackQueryHandler(self._daily, pattern='update_daily'),
|
||||||
|
CallbackQueryHandler(self._weekly, pattern='update_weekly'),
|
||||||
|
CallbackQueryHandler(self._monthly, pattern='update_monthly'),
|
||||||
CallbackQueryHandler(self._profit, pattern='update_profit'),
|
CallbackQueryHandler(self._profit, pattern='update_profit'),
|
||||||
CallbackQueryHandler(self._balance, pattern='update_balance'),
|
CallbackQueryHandler(self._balance, pattern='update_balance'),
|
||||||
CallbackQueryHandler(self._performance, pattern='update_performance'),
|
CallbackQueryHandler(self._performance, pattern='update_performance'),
|
||||||
|
CallbackQueryHandler(self._buy_tag_performance, pattern='update_buy_tag_performance'),
|
||||||
|
CallbackQueryHandler(self._sell_reason_performance,
|
||||||
|
pattern='update_sell_reason_performance'),
|
||||||
|
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
||||||
CallbackQueryHandler(self._count, pattern='update_count'),
|
CallbackQueryHandler(self._count, pattern='update_count'),
|
||||||
CallbackQueryHandler(self._forcebuy_inline),
|
CallbackQueryHandler(self._forcebuy_inline),
|
||||||
]
|
]
|
||||||
@ -208,26 +220,28 @@ class Telegram(RPCHandler):
|
|||||||
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||||
else:
|
else:
|
||||||
msg['stake_amount_fiat'] = 0
|
msg['stake_amount_fiat'] = 0
|
||||||
|
is_fill = msg['type'] == RPCMessageType.BUY_FILL
|
||||||
|
emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}'
|
||||||
|
|
||||||
content = []
|
message = (
|
||||||
content.append(
|
f"{emoji} *{msg['exchange']}:* {'Bought' if is_fill else 'Buying'} {msg['pair']}"
|
||||||
f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
|
|
||||||
f" (#{msg['trade_id']})\n"
|
f" (#{msg['trade_id']})\n"
|
||||||
)
|
|
||||||
if msg.get('buy_tag', None):
|
|
||||||
content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n")
|
|
||||||
content.append(f"*Amount:* `{msg['amount']:.8f}`\n")
|
|
||||||
content.append(f"*Open Rate:* `{msg['limit']:.8f}`\n")
|
|
||||||
content.append(f"*Current Rate:* `{msg['current_rate']:.8f}`\n")
|
|
||||||
content.append(
|
|
||||||
f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
|
|
||||||
)
|
|
||||||
if msg.get('fiat_currency', None):
|
|
||||||
content.append(
|
|
||||||
f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
|
||||||
)
|
)
|
||||||
|
message += f"*Buy Tag:* `{msg['buy_tag']}`\n" if msg.get('buy_tag', None) else ""
|
||||||
|
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||||
|
|
||||||
|
if msg['type'] == RPCMessageType.BUY_FILL:
|
||||||
|
message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
|
||||||
|
|
||||||
|
elif msg['type'] == RPCMessageType.BUY:
|
||||||
|
message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\
|
||||||
|
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
||||||
|
|
||||||
|
message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
|
||||||
|
|
||||||
|
if msg.get('fiat_currency', None):
|
||||||
|
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
||||||
|
|
||||||
message = ''.join(content)
|
|
||||||
message += ")`"
|
message += ")`"
|
||||||
return message
|
return message
|
||||||
|
|
||||||
@ -238,6 +252,7 @@ class Telegram(RPCHandler):
|
|||||||
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
||||||
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
||||||
|
|
||||||
|
msg['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None
|
||||||
msg['emoji'] = self._get_sell_emoji(msg)
|
msg['emoji'] = self._get_sell_emoji(msg)
|
||||||
|
|
||||||
# Check if all sell properties are available.
|
# Check if all sell properties are available.
|
||||||
@ -246,53 +261,57 @@ class Telegram(RPCHandler):
|
|||||||
and self._rpc._fiat_converter):
|
and self._rpc._fiat_converter):
|
||||||
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
|
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||||
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||||
msg['profit_extra'] = (' ({gain}: {profit_amount:.8f} {stake_currency}'
|
msg['profit_extra'] = (
|
||||||
' / {profit_fiat:.3f} {fiat_currency})').format(**msg)
|
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
|
||||||
|
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})")
|
||||||
else:
|
else:
|
||||||
msg['profit_extra'] = ''
|
msg['profit_extra'] = ''
|
||||||
|
is_fill = msg['type'] == RPCMessageType.SELL_FILL
|
||||||
|
message = (
|
||||||
|
f"{msg['emoji']} *{msg['exchange']}:* "
|
||||||
|
f"{'Sold' if is_fill else 'Selling'} {msg['pair']} (#{msg['trade_id']})\n"
|
||||||
|
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
|
||||||
|
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
||||||
|
f"*Buy Tag:* `{msg['buy_tag']}`\n"
|
||||||
|
f"*Sell Reason:* `{msg['sell_reason']}`\n"
|
||||||
|
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
|
||||||
|
f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||||
|
f"*Open Rate:* `{msg['open_rate']:.8f}`\n")
|
||||||
|
|
||||||
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
|
if msg['type'] == RPCMessageType.SELL:
|
||||||
"*Profit:* `{profit_percent:.2f}%{profit_extra}`\n"
|
message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
||||||
"*Sell Reason:* `{sell_reason}`\n"
|
f"*Close Rate:* `{msg['limit']:.8f}`")
|
||||||
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
|
|
||||||
"*Amount:* `{amount:.8f}`\n"
|
elif msg['type'] == RPCMessageType.SELL_FILL:
|
||||||
"*Open Rate:* `{open_rate:.8f}`\n"
|
message += f"*Close Rate:* `{msg['close_rate']:.8f}`"
|
||||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
|
||||||
"*Close Rate:* `{limit:.8f}`").format(**msg)
|
|
||||||
|
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
|
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
|
||||||
|
if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL]:
|
||||||
if msg_type == RPCMessageType.BUY:
|
|
||||||
message = self._format_buy_msg(msg)
|
message = self._format_buy_msg(msg)
|
||||||
|
|
||||||
|
elif msg_type in [RPCMessageType.SELL, RPCMessageType.SELL_FILL]:
|
||||||
|
message = self._format_sell_msg(msg)
|
||||||
|
|
||||||
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
|
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
|
||||||
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
|
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
|
||||||
message = ("\N{WARNING SIGN} *{exchange}:* "
|
message = ("\N{WARNING SIGN} *{exchange}:* "
|
||||||
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
|
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
|
||||||
"Reason: {reason}.".format(**msg))
|
"Reason: {reason}.".format(**msg))
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.BUY_FILL:
|
|
||||||
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
|
||||||
"Buy order for {pair} (#{trade_id}) filled "
|
|
||||||
"for {open_rate}.".format(**msg))
|
|
||||||
elif msg_type == RPCMessageType.SELL_FILL:
|
|
||||||
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
|
||||||
"Sell order for {pair} (#{trade_id}) filled "
|
|
||||||
"for {close_rate}.".format(**msg))
|
|
||||||
elif msg_type == RPCMessageType.SELL:
|
|
||||||
message = self._format_sell_msg(msg)
|
|
||||||
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
|
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
|
||||||
message = (
|
message = (
|
||||||
"*Protection* triggered due to {reason}. "
|
"*Protection* triggered due to {reason}. "
|
||||||
"`{pair}` will be locked until `{lock_end_time}`."
|
"`{pair}` will be locked until `{lock_end_time}`."
|
||||||
).format(**msg)
|
).format(**msg)
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
|
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
|
||||||
message = (
|
message = (
|
||||||
"*Protection* triggered due to {reason}. "
|
"*Protection* triggered due to {reason}. "
|
||||||
"*All pairs* will be locked until `{lock_end_time}`."
|
"*All pairs* will be locked until `{lock_end_time}`."
|
||||||
).format(**msg)
|
).format(**msg)
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.STATUS:
|
elif msg_type == RPCMessageType.STATUS:
|
||||||
message = '*Status:* `{status}`'.format(**msg)
|
message = '*Status:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
@ -344,7 +363,7 @@ class Telegram(RPCHandler):
|
|||||||
elif float(msg['profit_percent']) >= 0.0:
|
elif float(msg['profit_percent']) >= 0.0:
|
||||||
return "\N{EIGHT SPOKED ASTERISK}"
|
return "\N{EIGHT SPOKED ASTERISK}"
|
||||||
elif msg['sell_reason'] == "stop_loss":
|
elif msg['sell_reason'] == "stop_loss":
|
||||||
return"\N{WARNING SIGN}"
|
return "\N{WARNING SIGN}"
|
||||||
else:
|
else:
|
||||||
return "\N{CROSS MARK}"
|
return "\N{CROSS MARK}"
|
||||||
|
|
||||||
@ -384,19 +403,19 @@ class Telegram(RPCHandler):
|
|||||||
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
||||||
"*Current Rate:* `{current_rate:.8f}`",
|
"*Current Rate:* `{current_rate:.8f}`",
|
||||||
("*Current Profit:* " if r['is_open'] else "*Close Profit: *")
|
("*Current Profit:* " if r['is_open'] else "*Close Profit: *")
|
||||||
+ "`{profit_pct:.2f}%`",
|
+ "`{profit_ratio:.2%}`",
|
||||||
]
|
]
|
||||||
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
|
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
|
||||||
and r['initial_stop_loss_pct'] is not None):
|
and r['initial_stop_loss_ratio'] is not None):
|
||||||
# Adding initial stoploss only if it is different from stoploss
|
# Adding initial stoploss only if it is different from stoploss
|
||||||
lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` "
|
lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` "
|
||||||
"`({initial_stop_loss_pct:.2f}%)`")
|
"`({initial_stop_loss_ratio:.2%})`")
|
||||||
|
|
||||||
# Adding stoploss and stoploss percentage only if it is not None
|
# Adding stoploss and stoploss percentage only if it is not None
|
||||||
lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " +
|
lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " +
|
||||||
("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""))
|
("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else ""))
|
||||||
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
|
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
|
||||||
"`({stoploss_current_dist_pct:.2f}%)`")
|
"`({stoploss_current_dist_ratio:.2%})`")
|
||||||
if r['open_order']:
|
if r['open_order']:
|
||||||
if r['sell_order_status']:
|
if r['sell_order_status']:
|
||||||
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`")
|
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`")
|
||||||
@ -492,6 +511,86 @@ class Telegram(RPCHandler):
|
|||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _weekly(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /weekly <n>
|
||||||
|
Returns a weekly profit (in BTC) over the last n weeks.
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
stake_cur = self._config['stake_currency']
|
||||||
|
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||||
|
try:
|
||||||
|
timescale = int(context.args[0]) if context.args else 8
|
||||||
|
except (TypeError, ValueError, IndexError):
|
||||||
|
timescale = 8
|
||||||
|
try:
|
||||||
|
stats = self._rpc._rpc_weekly_profit(
|
||||||
|
timescale,
|
||||||
|
stake_cur,
|
||||||
|
fiat_disp_cur
|
||||||
|
)
|
||||||
|
stats_tab = tabulate(
|
||||||
|
[[week['date'],
|
||||||
|
f"{round_coin_value(week['abs_profit'], stats['stake_currency'])}",
|
||||||
|
f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
||||||
|
f"{week['trade_count']} trades"] for week in stats['data']],
|
||||||
|
headers=[
|
||||||
|
'Monday',
|
||||||
|
f'Profit {stake_cur}',
|
||||||
|
f'Profit {fiat_disp_cur}',
|
||||||
|
'Trades',
|
||||||
|
],
|
||||||
|
tablefmt='simple')
|
||||||
|
message = f'<b>Weekly Profit over the last {timescale} weeks ' \
|
||||||
|
f'(starting from Monday)</b>:\n<pre>{stats_tab}</pre> '
|
||||||
|
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
||||||
|
callback_path="update_weekly", query=update.callback_query)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e))
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _monthly(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /monthly <n>
|
||||||
|
Returns a monthly profit (in BTC) over the last n months.
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
stake_cur = self._config['stake_currency']
|
||||||
|
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||||
|
try:
|
||||||
|
timescale = int(context.args[0]) if context.args else 6
|
||||||
|
except (TypeError, ValueError, IndexError):
|
||||||
|
timescale = 6
|
||||||
|
try:
|
||||||
|
stats = self._rpc._rpc_monthly_profit(
|
||||||
|
timescale,
|
||||||
|
stake_cur,
|
||||||
|
fiat_disp_cur
|
||||||
|
)
|
||||||
|
stats_tab = tabulate(
|
||||||
|
[[month['date'],
|
||||||
|
f"{round_coin_value(month['abs_profit'], stats['stake_currency'])}",
|
||||||
|
f"{month['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
||||||
|
f"{month['trade_count']} trades"] for month in stats['data']],
|
||||||
|
headers=[
|
||||||
|
'Month',
|
||||||
|
f'Profit {stake_cur}',
|
||||||
|
f'Profit {fiat_disp_cur}',
|
||||||
|
'Trades',
|
||||||
|
],
|
||||||
|
tablefmt='simple')
|
||||||
|
message = f'<b>Monthly Profit over the last {timescale} months' \
|
||||||
|
f'</b>:\n<pre>{stats_tab}</pre> '
|
||||||
|
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
||||||
|
callback_path="update_monthly", query=update.callback_query)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _profit(self, update: Update, context: CallbackContext) -> None:
|
def _profit(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
@ -519,11 +618,11 @@ class Telegram(RPCHandler):
|
|||||||
fiat_disp_cur,
|
fiat_disp_cur,
|
||||||
start_date)
|
start_date)
|
||||||
profit_closed_coin = stats['profit_closed_coin']
|
profit_closed_coin = stats['profit_closed_coin']
|
||||||
profit_closed_percent_mean = stats['profit_closed_percent_mean']
|
profit_closed_ratio_mean = stats['profit_closed_ratio_mean']
|
||||||
profit_closed_percent = stats['profit_closed_percent']
|
profit_closed_percent = stats['profit_closed_percent']
|
||||||
profit_closed_fiat = stats['profit_closed_fiat']
|
profit_closed_fiat = stats['profit_closed_fiat']
|
||||||
profit_all_coin = stats['profit_all_coin']
|
profit_all_coin = stats['profit_all_coin']
|
||||||
profit_all_percent_mean = stats['profit_all_percent_mean']
|
profit_all_ratio_mean = stats['profit_all_ratio_mean']
|
||||||
profit_all_percent = stats['profit_all_percent']
|
profit_all_percent = stats['profit_all_percent']
|
||||||
profit_all_fiat = stats['profit_all_fiat']
|
profit_all_fiat = stats['profit_all_fiat']
|
||||||
trade_count = stats['trade_count']
|
trade_count = stats['trade_count']
|
||||||
@ -531,7 +630,7 @@ class Telegram(RPCHandler):
|
|||||||
latest_trade_date = stats['latest_trade_date']
|
latest_trade_date = stats['latest_trade_date']
|
||||||
avg_duration = stats['avg_duration']
|
avg_duration = stats['avg_duration']
|
||||||
best_pair = stats['best_pair']
|
best_pair = stats['best_pair']
|
||||||
best_rate = stats['best_rate']
|
best_pair_profit_ratio = stats['best_pair_profit_ratio']
|
||||||
if stats['trade_count'] == 0:
|
if stats['trade_count'] == 0:
|
||||||
markdown_msg = 'No trades yet.'
|
markdown_msg = 'No trades yet.'
|
||||||
else:
|
else:
|
||||||
@ -539,7 +638,7 @@ class Telegram(RPCHandler):
|
|||||||
if stats['closed_trade_count'] > 0:
|
if stats['closed_trade_count'] > 0:
|
||||||
markdown_msg = ("*ROI:* Closed trades\n"
|
markdown_msg = ("*ROI:* Closed trades\n"
|
||||||
f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} "
|
f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} "
|
||||||
f"({profit_closed_percent_mean:.2f}%) "
|
f"({profit_closed_ratio_mean:.2%}) "
|
||||||
f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
||||||
f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n")
|
f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n")
|
||||||
else:
|
else:
|
||||||
@ -548,7 +647,7 @@ class Telegram(RPCHandler):
|
|||||||
markdown_msg += (
|
markdown_msg += (
|
||||||
f"*ROI:* All trades\n"
|
f"*ROI:* All trades\n"
|
||||||
f"∙ `{round_coin_value(profit_all_coin, stake_cur)} "
|
f"∙ `{round_coin_value(profit_all_coin, stake_cur)} "
|
||||||
f"({profit_all_percent_mean:.2f}%) "
|
f"({profit_all_ratio_mean:.2%}) "
|
||||||
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
||||||
f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
|
f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
|
||||||
f"*Total Trade Count:* `{trade_count}`\n"
|
f"*Total Trade Count:* `{trade_count}`\n"
|
||||||
@ -559,7 +658,7 @@ class Telegram(RPCHandler):
|
|||||||
)
|
)
|
||||||
if stats['closed_trade_count'] > 0:
|
if stats['closed_trade_count'] > 0:
|
||||||
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
|
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
|
||||||
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
|
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`")
|
||||||
self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
|
self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
|
|
||||||
@ -588,10 +687,16 @@ class Telegram(RPCHandler):
|
|||||||
count['losses']
|
count['losses']
|
||||||
] for reason, count in stats['sell_reasons'].items()
|
] for reason, count in stats['sell_reasons'].items()
|
||||||
]
|
]
|
||||||
sell_reasons_msg = tabulate(
|
sell_reasons_msg = 'No trades yet.'
|
||||||
sell_reasons_tabulate,
|
for reason in chunks(sell_reasons_tabulate, 25):
|
||||||
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
|
sell_reasons_msg = tabulate(
|
||||||
)
|
reason,
|
||||||
|
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
|
||||||
|
)
|
||||||
|
if len(sell_reasons_tabulate) > 25:
|
||||||
|
self._send_msg(sell_reasons_msg, ParseMode.MARKDOWN)
|
||||||
|
sell_reasons_msg = ''
|
||||||
|
|
||||||
durations = stats['durations']
|
durations = stats['durations']
|
||||||
duration_msg = tabulate(
|
duration_msg = tabulate(
|
||||||
[
|
[
|
||||||
@ -662,10 +767,10 @@ class Telegram(RPCHandler):
|
|||||||
output += ("\n*Estimated Value*:\n"
|
output += ("\n*Estimated Value*:\n"
|
||||||
f"\t`{result['stake']}: "
|
f"\t`{result['stake']}: "
|
||||||
f"{round_coin_value(result['total'], result['stake'], False)}`"
|
f"{round_coin_value(result['total'], result['stake'], False)}`"
|
||||||
f" `({result['starting_capital_pct']}%)`\n"
|
f" `({result['starting_capital_ratio']:.2%})`\n"
|
||||||
f"\t`{result['symbol']}: "
|
f"\t`{result['symbol']}: "
|
||||||
f"{round_coin_value(result['value'], result['symbol'], False)}`"
|
f"{round_coin_value(result['value'], result['symbol'], False)}`"
|
||||||
f" `({result['starting_capital_fiat_pct']}%)`\n")
|
f" `({result['starting_capital_fiat_ratio']:.2%})`\n")
|
||||||
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
@ -800,7 +905,7 @@ class Telegram(RPCHandler):
|
|||||||
trades_tab = tabulate(
|
trades_tab = tabulate(
|
||||||
[[arrow.get(trade['close_date']).humanize(),
|
[[arrow.get(trade['close_date']).humanize(),
|
||||||
trade['pair'] + " (#" + str(trade['trade_id']) + ")",
|
trade['pair'] + " (#" + str(trade['trade_id']) + ")",
|
||||||
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
|
f"{(trade['close_profit']):.2%} ({trade['close_profit_abs']})"]
|
||||||
for trade in trades['trades']],
|
for trade in trades['trades']],
|
||||||
headers=[
|
headers=[
|
||||||
'Close Date',
|
'Close Date',
|
||||||
@ -852,7 +957,7 @@ class Telegram(RPCHandler):
|
|||||||
stat_line = (
|
stat_line = (
|
||||||
f"{i+1}.\t <code>{trade['pair']}\t"
|
f"{i+1}.\t <code>{trade['pair']}\t"
|
||||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
f"({trade['profit']:.2f}%) "
|
f"({trade['profit_ratio']:.2%}) "
|
||||||
f"({trade['count']})</code>\n")
|
f"({trade['count']})</code>\n")
|
||||||
|
|
||||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||||
@ -867,6 +972,111 @@ class Telegram(RPCHandler):
|
|||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _buy_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /buys PAIR .
|
||||||
|
Shows a performance statistic from finished trades
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
pair = None
|
||||||
|
if context.args and isinstance(context.args[0], str):
|
||||||
|
pair = context.args[0]
|
||||||
|
|
||||||
|
trades = self._rpc._rpc_buy_tag_performance(pair)
|
||||||
|
output = "<b>Buy Tag Performance:</b>\n"
|
||||||
|
for i, trade in enumerate(trades):
|
||||||
|
stat_line = (
|
||||||
|
f"{i+1}.\t <code>{trade['buy_tag']}\t"
|
||||||
|
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
|
f"({trade['profit_ratio']:.2%}) "
|
||||||
|
f"({trade['count']})</code>\n")
|
||||||
|
|
||||||
|
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||||
|
self._send_msg(output, parse_mode=ParseMode.HTML)
|
||||||
|
output = stat_line
|
||||||
|
else:
|
||||||
|
output += stat_line
|
||||||
|
|
||||||
|
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||||
|
reload_able=True, callback_path="update_buy_tag_performance",
|
||||||
|
query=update.callback_query)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e))
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _sell_reason_performance(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /sells.
|
||||||
|
Shows a performance statistic from finished trades
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
pair = None
|
||||||
|
if context.args and isinstance(context.args[0], str):
|
||||||
|
pair = context.args[0]
|
||||||
|
|
||||||
|
trades = self._rpc._rpc_sell_reason_performance(pair)
|
||||||
|
output = "<b>Sell Reason Performance:</b>\n"
|
||||||
|
for i, trade in enumerate(trades):
|
||||||
|
stat_line = (
|
||||||
|
f"{i+1}.\t <code>{trade['sell_reason']}\t"
|
||||||
|
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
|
f"({trade['profit_ratio']:.2%}) "
|
||||||
|
f"({trade['count']})</code>\n")
|
||||||
|
|
||||||
|
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||||
|
self._send_msg(output, parse_mode=ParseMode.HTML)
|
||||||
|
output = stat_line
|
||||||
|
else:
|
||||||
|
output += stat_line
|
||||||
|
|
||||||
|
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||||
|
reload_able=True, callback_path="update_sell_reason_performance",
|
||||||
|
query=update.callback_query)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e))
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /mix_tags.
|
||||||
|
Shows a performance statistic from finished trades
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
pair = None
|
||||||
|
if context.args and isinstance(context.args[0], str):
|
||||||
|
pair = context.args[0]
|
||||||
|
|
||||||
|
trades = self._rpc._rpc_mix_tag_performance(pair)
|
||||||
|
output = "<b>Mix Tag Performance:</b>\n"
|
||||||
|
for i, trade in enumerate(trades):
|
||||||
|
stat_line = (
|
||||||
|
f"{i+1}.\t <code>{trade['mix_tag']}\t"
|
||||||
|
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
|
f"({trade['profit']:.2%}) "
|
||||||
|
f"({trade['count']})</code>\n")
|
||||||
|
|
||||||
|
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||||
|
self._send_msg(output, parse_mode=ParseMode.HTML)
|
||||||
|
output = stat_line
|
||||||
|
else:
|
||||||
|
output += stat_line
|
||||||
|
|
||||||
|
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||||
|
reload_able=True, callback_path="update_mix_tag_performance",
|
||||||
|
query=update.callback_query)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _count(self, update: Update, context: CallbackContext) -> None:
|
def _count(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1033,42 +1243,58 @@ class Telegram(RPCHandler):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. "
|
forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. "
|
||||||
"Optionally takes a rate at which to buy.` \n")
|
"Optionally takes a rate at which to buy "
|
||||||
message = ("*/start:* `Starts the trader`\n"
|
"(only applies to limit orders).` \n")
|
||||||
"*/stop:* `Stops the trader`\n"
|
message = (
|
||||||
"*/status <trade_id>|[table]:* `Lists all open trades`\n"
|
"_BotControl_\n"
|
||||||
" *<trade_id> :* `Lists one or more specific trades.`\n"
|
"------------\n"
|
||||||
" `Separate multiple <trade_id> with a blank space.`\n"
|
"*/start:* `Starts the trader`\n"
|
||||||
" *table :* `will display trades in a table`\n"
|
"*/stop:* Stops the trader\n"
|
||||||
" `pending buy orders are marked with an asterisk (*)`\n"
|
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
|
||||||
" `pending sell orders are marked with a double asterisk (**)`\n"
|
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
|
||||||
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
"regardless of profit`\n"
|
||||||
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
|
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}"
|
||||||
"over the last n days`\n"
|
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
|
||||||
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
|
"*/whitelist:* `Show current whitelist` \n"
|
||||||
"regardless of profit`\n"
|
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
|
||||||
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}"
|
"to the blacklist.` \n"
|
||||||
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
|
"*/reload_config:* `Reload configuration file` \n"
|
||||||
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
|
"*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
|
||||||
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
|
|
||||||
"*/stats:* `Shows Wins / losses by Sell reason as well as "
|
|
||||||
"Avg. holding durationsfor buys and sells.`\n"
|
|
||||||
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
|
|
||||||
"*/locks:* `Show currently locked pairs`\n"
|
|
||||||
"*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
|
|
||||||
"*/balance:* `Show account balance per currency`\n"
|
|
||||||
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
|
|
||||||
"*/reload_config:* `Reload configuration file` \n"
|
|
||||||
"*/show_config:* `Show running configuration` \n"
|
|
||||||
"*/logs [limit]:* `Show latest logs - defaults to 10` \n"
|
|
||||||
"*/whitelist:* `Show current whitelist` \n"
|
|
||||||
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
|
|
||||||
"to the blacklist.` \n"
|
|
||||||
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
|
|
||||||
"*/help:* `This help message`\n"
|
|
||||||
"*/version:* `Show version`")
|
|
||||||
|
|
||||||
self._send_msg(message)
|
"_Current state_\n"
|
||||||
|
"------------\n"
|
||||||
|
"*/show_config:* `Show running configuration` \n"
|
||||||
|
"*/locks:* `Show currently locked pairs`\n"
|
||||||
|
"*/balance:* `Show account balance per currency`\n"
|
||||||
|
"*/logs [limit]:* `Show latest logs - defaults to 10` \n"
|
||||||
|
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
|
||||||
|
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
|
||||||
|
|
||||||
|
"_Statistics_\n"
|
||||||
|
"------------\n"
|
||||||
|
"*/status <trade_id>|[table]:* `Lists all open trades`\n"
|
||||||
|
" *<trade_id> :* `Lists one or more specific trades.`\n"
|
||||||
|
" `Separate multiple <trade_id> with a blank space.`\n"
|
||||||
|
" *table :* `will display trades in a table`\n"
|
||||||
|
" `pending buy orders are marked with an asterisk (*)`\n"
|
||||||
|
" `pending sell orders are marked with a double asterisk (**)`\n"
|
||||||
|
"*/buys <pair|none>:* `Shows the buy_tag performance`\n"
|
||||||
|
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
|
||||||
|
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
|
||||||
|
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
||||||
|
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
|
||||||
|
"over the last n days`\n"
|
||||||
|
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
|
||||||
|
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
|
||||||
|
"*/weekly <n>:* `Shows statistics per week, over the last n weeks`\n"
|
||||||
|
"*/monthly <n>:* `Shows statistics per month, over the last n months`\n"
|
||||||
|
"*/stats:* `Shows Wins / losses by Sell reason as well as "
|
||||||
|
"Avg. holding durationsfor buys and sells.`\n"
|
||||||
|
"*/help:* `This help message`\n"
|
||||||
|
"*/version:* `Show version`"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._send_msg(message, parse_mode=ParseMode.MARKDOWN)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _version(self, update: Update, context: CallbackContext) -> None:
|
def _version(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
This module manages webhook communication
|
This module manages webhook communication
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from requests import RequestException, post
|
from requests import RequestException, post
|
||||||
@ -28,12 +29,9 @@ class Webhook(RPCHandler):
|
|||||||
super().__init__(rpc, config)
|
super().__init__(rpc, config)
|
||||||
|
|
||||||
self._url = self._config['webhook']['url']
|
self._url = self._config['webhook']['url']
|
||||||
|
|
||||||
self._format = self._config['webhook'].get('format', 'form')
|
self._format = self._config['webhook'].get('format', 'form')
|
||||||
|
self._retries = self._config['webhook'].get('retries', 0)
|
||||||
if self._format != 'form' and self._format != 'json':
|
self._retry_delay = self._config['webhook'].get('retry_delay', 0.1)
|
||||||
raise NotImplementedError('Unknown webhook format `{}`, possible values are '
|
|
||||||
'`form` (default) and `json`'.format(self._format))
|
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -77,13 +75,30 @@ class Webhook(RPCHandler):
|
|||||||
def _send_msg(self, payload: dict) -> None:
|
def _send_msg(self, payload: dict) -> None:
|
||||||
"""do the actual call to the webhook"""
|
"""do the actual call to the webhook"""
|
||||||
|
|
||||||
try:
|
success = False
|
||||||
if self._format == 'form':
|
attempts = 0
|
||||||
post(self._url, data=payload)
|
while not success and attempts <= self._retries:
|
||||||
elif self._format == 'json':
|
if attempts:
|
||||||
post(self._url, json=payload)
|
if self._retry_delay:
|
||||||
else:
|
time.sleep(self._retry_delay)
|
||||||
raise NotImplementedError('Unknown format: {}'.format(self._format))
|
logger.info("Retrying webhook...")
|
||||||
|
|
||||||
except RequestException as exc:
|
attempts += 1
|
||||||
logger.warning("Could not call webhook url. Exception: %s", exc)
|
|
||||||
|
try:
|
||||||
|
if self._format == 'form':
|
||||||
|
response = post(self._url, data=payload)
|
||||||
|
elif self._format == 'json':
|
||||||
|
response = post(self._url, json=payload)
|
||||||
|
elif self._format == 'raw':
|
||||||
|
response = post(self._url, data=payload['data'],
|
||||||
|
headers={'Content-Type': 'text/plain'})
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('Unknown format: {}'.format(self._format))
|
||||||
|
|
||||||
|
# Throw a RequestException if the post was not successful
|
||||||
|
response.raise_for_status()
|
||||||
|
success = True
|
||||||
|
|
||||||
|
except RequestException as exc:
|
||||||
|
logger.warning("Could not call webhook url. Exception: %s", exc)
|
||||||
|
@ -292,7 +292,7 @@ class BooleanParameter(CategoricalParameter):
|
|||||||
load=load, **kwargs)
|
load=load, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class HyperStrategyMixin(object):
|
class HyperStrategyMixin:
|
||||||
"""
|
"""
|
||||||
A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell
|
A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell
|
||||||
strategy logic.
|
strategy logic.
|
||||||
@ -381,7 +381,8 @@ class HyperStrategyMixin(object):
|
|||||||
if filename.is_file():
|
if filename.is_file():
|
||||||
logger.info(f"Loading parameters from file {filename}")
|
logger.info(f"Loading parameters from file {filename}")
|
||||||
try:
|
try:
|
||||||
params = json_load(filename.open('r'))
|
with filename.open('r') as f:
|
||||||
|
params = json_load(f)
|
||||||
if params.get('strategy_name') != self.__class__.__name__:
|
if params.get('strategy_name') != self.__class__.__name__:
|
||||||
raise OperationalException('Invalid parameter file provided.')
|
raise OperationalException('Invalid parameter file provided.')
|
||||||
return params
|
return params
|
||||||
|
@ -80,12 +80,11 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata:
|
|||||||
# Not specifying an asset will define informative dataframe for current pair.
|
# Not specifying an asset will define informative dataframe for current pair.
|
||||||
asset = metadata['pair']
|
asset = metadata['pair']
|
||||||
|
|
||||||
if '/' in asset:
|
market = strategy.dp.market(asset)
|
||||||
base, quote = asset.split('/')
|
if market is None:
|
||||||
else:
|
raise OperationalException(f'Market {asset} is not available.')
|
||||||
# When futures are supported this may need reevaluation.
|
base = market['base']
|
||||||
# base, quote = asset, ''
|
quote = market['quote']
|
||||||
raise OperationalException('Not implemented.')
|
|
||||||
|
|
||||||
# Default format. This optimizes for the common case: informative pairs using same stake
|
# Default format. This optimizes for the common case: informative pairs using same stake
|
||||||
# currency. When quote currency matches stake currency, column name will omit base currency.
|
# currency. When quote currency matches stake currency, column name will omit base currency.
|
||||||
|
@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
|
|||||||
CUSTOM_SELL_MAX_LENGTH = 64
|
CUSTOM_SELL_MAX_LENGTH = 64
|
||||||
|
|
||||||
|
|
||||||
class SellCheckTuple(object):
|
class SellCheckTuple:
|
||||||
"""
|
"""
|
||||||
NamedTuple for Sell type + reason
|
NamedTuple for Sell type + reason
|
||||||
"""
|
"""
|
||||||
@ -65,9 +65,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
_populate_fun_len: int = 0
|
_populate_fun_len: int = 0
|
||||||
_buy_fun_len: int = 0
|
_buy_fun_len: int = 0
|
||||||
_sell_fun_len: int = 0
|
_sell_fun_len: int = 0
|
||||||
_ft_params_from_file: Dict = {}
|
_ft_params_from_file: Dict
|
||||||
# associated minimal roi
|
# associated minimal roi
|
||||||
minimal_roi: Dict
|
minimal_roi: Dict = {}
|
||||||
|
|
||||||
# associated stoploss
|
# associated stoploss
|
||||||
stoploss: float
|
stoploss: float
|
||||||
@ -443,6 +443,15 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
PairLocks.unlock_pair(pair, datetime.now(timezone.utc))
|
PairLocks.unlock_pair(pair, datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
def unlock_reason(self, reason: str) -> None:
|
||||||
|
"""
|
||||||
|
Unlocks all pairs previously locked using lock_pair with specified reason.
|
||||||
|
Not used by freqtrade itself, but intended to be used if users lock pairs
|
||||||
|
manually from within the strategy, to allow an easy way to unlock pairs.
|
||||||
|
:param reason: Unlock pairs to allow trading again
|
||||||
|
"""
|
||||||
|
PairLocks.unlock_reason(reason, datetime.now(timezone.utc))
|
||||||
|
|
||||||
def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool:
|
def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if a pair is currently locked
|
Checks if a pair is currently locked
|
||||||
@ -500,6 +509,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
dataframe['buy'] = 0
|
dataframe['buy'] = 0
|
||||||
dataframe['sell'] = 0
|
dataframe['sell'] = 0
|
||||||
dataframe['buy_tag'] = None
|
dataframe['buy_tag'] = None
|
||||||
|
dataframe['exit_tag'] = None
|
||||||
|
|
||||||
# Other Defs in strategy that want to be called every loop here
|
# Other Defs in strategy that want to be called every loop here
|
||||||
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
|
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
|
||||||
@ -577,7 +587,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
pair: str,
|
pair: str,
|
||||||
timeframe: str,
|
timeframe: str,
|
||||||
dataframe: DataFrame
|
dataframe: DataFrame
|
||||||
) -> Tuple[bool, bool, Optional[str]]:
|
) -> Tuple[bool, bool, Optional[str], Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Calculates current signal based based on the buy / sell columns of the dataframe.
|
Calculates current signal based based on the buy / sell columns of the dataframe.
|
||||||
Used by Bot to get the signal to buy or sell
|
Used by Bot to get the signal to buy or sell
|
||||||
@ -588,7 +598,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(dataframe, DataFrame) or dataframe.empty:
|
if not isinstance(dataframe, DataFrame) or dataframe.empty:
|
||||||
logger.warning(f'Empty candle (OHLCV) data for pair {pair}')
|
logger.warning(f'Empty candle (OHLCV) data for pair {pair}')
|
||||||
return False, False, None
|
return False, False, None, None
|
||||||
|
|
||||||
latest_date = dataframe['date'].max()
|
latest_date = dataframe['date'].max()
|
||||||
latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1]
|
latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1]
|
||||||
@ -603,7 +613,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
'Outdated history for pair %s. Last tick is %s minutes old',
|
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
pair, int((arrow.utcnow() - latest_date).total_seconds() // 60)
|
pair, int((arrow.utcnow() - latest_date).total_seconds() // 60)
|
||||||
)
|
)
|
||||||
return False, False, None
|
return False, False, None, None
|
||||||
|
|
||||||
buy = latest[SignalType.BUY.value] == 1
|
buy = latest[SignalType.BUY.value] == 1
|
||||||
|
|
||||||
@ -612,6 +622,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
sell = latest[SignalType.SELL.value] == 1
|
sell = latest[SignalType.SELL.value] == 1
|
||||||
|
|
||||||
buy_tag = latest.get(SignalTagType.BUY_TAG.value, None)
|
buy_tag = latest.get(SignalTagType.BUY_TAG.value, None)
|
||||||
|
exit_tag = latest.get(SignalTagType.EXIT_TAG.value, None)
|
||||||
|
|
||||||
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
|
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
|
||||||
latest['date'], pair, str(buy), str(sell))
|
latest['date'], pair, str(buy), str(sell))
|
||||||
@ -620,8 +631,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
current_time=datetime.now(timezone.utc),
|
current_time=datetime.now(timezone.utc),
|
||||||
timeframe_seconds=timeframe_seconds,
|
timeframe_seconds=timeframe_seconds,
|
||||||
buy=buy):
|
buy=buy):
|
||||||
return False, sell, buy_tag
|
return False, sell, buy_tag, exit_tag
|
||||||
return buy, sell, buy_tag
|
return buy, sell, buy_tag, exit_tag
|
||||||
|
|
||||||
def ignore_expired_candle(self, latest_date: datetime, current_time: datetime,
|
def ignore_expired_candle(self, latest_date: datetime, current_time: datetime,
|
||||||
timeframe_seconds: int, buy: bool):
|
timeframe_seconds: int, buy: bool):
|
||||||
@ -754,7 +765,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
if self.trailing_stop_positive is not None and high_profit > sl_offset:
|
if self.trailing_stop_positive is not None and high_profit > sl_offset:
|
||||||
stop_loss_value = self.trailing_stop_positive
|
stop_loss_value = self.trailing_stop_positive
|
||||||
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
|
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
|
||||||
f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")
|
f"offset: {sl_offset:.4g} profit: {current_profit:.2%}")
|
||||||
|
|
||||||
trade.adjust_stop_loss(high or current_rate, stop_loss_value)
|
trade.adjust_stop_loss(high or current_rate, stop_loss_value)
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
from freqtrade.exceptions import StrategyError
|
from freqtrade.exceptions import StrategyError
|
||||||
|
|
||||||
@ -14,6 +15,9 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_err
|
|||||||
"""
|
"""
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
if 'trade' in kwargs:
|
||||||
|
# Protect accidental modifications from within the strategy
|
||||||
|
kwargs['trade'] = deepcopy(kwargs['trade'])
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
@ -10,8 +10,7 @@
|
|||||||
"stake_currency": "{{ stake_currency }}",
|
"stake_currency": "{{ stake_currency }}",
|
||||||
"stake_amount": {{ stake_amount }},
|
"stake_amount": {{ stake_amount }},
|
||||||
"tradable_balance_ratio": 0.99,
|
"tradable_balance_ratio": 0.99,
|
||||||
"fiat_display_currency": "{{ fiat_display_currency }}",
|
"fiat_display_currency": "{{ fiat_display_currency }}",{{ ('\n "timeframe": "' + timeframe + '",') if timeframe else '' }}
|
||||||
"timeframe": "{{ timeframe }}",
|
|
||||||
"dry_run": {{ dry_run | lower }},
|
"dry_run": {{ dry_run | lower }},
|
||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
|
@ -12,6 +12,7 @@ from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalP
|
|||||||
# --------------------------------
|
# --------------------------------
|
||||||
# Add your lib to import here
|
# Add your lib to import here
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
import pandas_ta as pta
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
|
||||||
|
|
||||||
@ -36,6 +37,9 @@ class {{ strategy }}(IStrategy):
|
|||||||
# Check the documentation or the Sample strategy to get the latest version.
|
# Check the documentation or the Sample strategy to get the latest version.
|
||||||
INTERFACE_VERSION = 2
|
INTERFACE_VERSION = 2
|
||||||
|
|
||||||
|
# Optimal timeframe for the strategy.
|
||||||
|
timeframe = '5m'
|
||||||
|
|
||||||
# Minimal ROI designed for the strategy.
|
# Minimal ROI designed for the strategy.
|
||||||
# This attribute will be overridden if the config file contains "minimal_roi".
|
# This attribute will be overridden if the config file contains "minimal_roi".
|
||||||
minimal_roi = {
|
minimal_roi = {
|
||||||
@ -54,9 +58,6 @@ class {{ strategy }}(IStrategy):
|
|||||||
# trailing_stop_positive = 0.01
|
# trailing_stop_positive = 0.01
|
||||||
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
|
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
|
||||||
|
|
||||||
# Optimal timeframe for the strategy.
|
|
||||||
timeframe = '5m'
|
|
||||||
|
|
||||||
# Run "populate_indicators()" only for new candle.
|
# Run "populate_indicators()" only for new candle.
|
||||||
process_only_new_candles = False
|
process_only_new_candles = False
|
||||||
|
|
||||||
@ -68,6 +69,10 @@ class {{ strategy }}(IStrategy):
|
|||||||
# Number of candles the strategy requires before producing valid signals
|
# Number of candles the strategy requires before producing valid signals
|
||||||
startup_candle_count: int = 30
|
startup_candle_count: int = 30
|
||||||
|
|
||||||
|
# Strategy parameters
|
||||||
|
buy_rsi = IntParameter(10, 40, default=30, space="buy")
|
||||||
|
sell_rsi = IntParameter(60, 90, default=70, space="sell")
|
||||||
|
|
||||||
# Optional order type mapping.
|
# Optional order type mapping.
|
||||||
order_types = {
|
order_types = {
|
||||||
'buy': 'limit',
|
'buy': 'limit',
|
||||||
@ -82,6 +87,7 @@ class {{ strategy }}(IStrategy):
|
|||||||
'sell': 'gtc'
|
'sell': 'gtc'
|
||||||
}
|
}
|
||||||
{{ plot_config | indent(4) }}
|
{{ plot_config | indent(4) }}
|
||||||
|
|
||||||
def informative_pairs(self):
|
def informative_pairs(self):
|
||||||
"""
|
"""
|
||||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||||
|
@ -79,7 +79,9 @@
|
|||||||
"source": [
|
"source": [
|
||||||
"# Load strategy using values set above\n",
|
"# Load strategy using values set above\n",
|
||||||
"from freqtrade.resolvers import StrategyResolver\n",
|
"from freqtrade.resolvers import StrategyResolver\n",
|
||||||
|
"from freqtrade.data.dataprovider import DataProvider\n",
|
||||||
"strategy = StrategyResolver.load_strategy(config)\n",
|
"strategy = StrategyResolver.load_strategy(config)\n",
|
||||||
|
"strategy.dp = DataProvider(config, None, None)\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# Generate buy/sell signals using strategy\n",
|
"# Generate buy/sell signals using strategy\n",
|
||||||
"df = strategy.analyze_ticker(candles, {'pair': pair})\n",
|
"df = strategy.analyze_ticker(candles, {'pair': pair})\n",
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
|
(qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)) & # Signal: RSI crosses above buy_rsi
|
||||||
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle
|
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle
|
||||||
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
|
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
|
||||||
|
@ -1 +1 @@
|
|||||||
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
|
(qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)) & # Signal: RSI crosses above buy_rsi
|
||||||
|
12
freqtrade/templates/subtemplates/exchange_okex.j2
Normal file
12
freqtrade/templates/subtemplates/exchange_okex.j2
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"exchange": {
|
||||||
|
"name": "{{ exchange_name | lower }}",
|
||||||
|
"key": "{{ exchange_key }}",
|
||||||
|
"secret": "{{ exchange_secret }}",
|
||||||
|
"password": "{{ exchange_key_password }}",
|
||||||
|
"ccxt_config": {},
|
||||||
|
"ccxt_async_config": {},
|
||||||
|
"pair_whitelist": [
|
||||||
|
],
|
||||||
|
"pair_blacklist": [
|
||||||
|
]
|
||||||
|
}
|
@ -1,18 +1,20 @@
|
|||||||
|
|
||||||
plot_config = {
|
@property
|
||||||
# Main plot indicators (Moving averages, ...)
|
def plot_config(self):
|
||||||
'main_plot': {
|
return {
|
||||||
'tema': {},
|
# Main plot indicators (Moving averages, ...)
|
||||||
'sar': {'color': 'white'},
|
'main_plot': {
|
||||||
},
|
'tema': {},
|
||||||
'subplots': {
|
'sar': {'color': 'white'},
|
||||||
# Subplots - each dict defines one additional plot
|
|
||||||
"MACD": {
|
|
||||||
'macd': {'color': 'blue'},
|
|
||||||
'macdsignal': {'color': 'orange'},
|
|
||||||
},
|
},
|
||||||
"RSI": {
|
'subplots': {
|
||||||
'rsi': {'color': 'red'},
|
# Subplots - each dict defines one additional plot
|
||||||
|
"MACD": {
|
||||||
|
'macd': {'color': 'blue'},
|
||||||
|
'macdsignal': {'color': 'orange'},
|
||||||
|
},
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70
|
(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) & # Signal: RSI crosses above sell_rsi
|
||||||
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle
|
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle
|
||||||
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
|
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
|
||||||
|
@ -1 +1 @@
|
|||||||
(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70
|
(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) & # Signal: RSI crosses above sell_rsi
|
||||||
|
@ -73,7 +73,7 @@ class Wallets:
|
|||||||
tot_profit = Trade.get_total_closed_profit()
|
tot_profit = Trade.get_total_closed_profit()
|
||||||
else:
|
else:
|
||||||
tot_profit = LocalTrade.total_profit
|
tot_profit = LocalTrade.total_profit
|
||||||
tot_in_trades = sum([trade.stake_amount for trade in open_trades])
|
tot_in_trades = sum(trade.stake_amount for trade in open_trades)
|
||||||
|
|
||||||
current_stake = self.start_cap + tot_profit - tot_in_trades
|
current_stake = self.start_cap + tot_profit - tot_in_trades
|
||||||
_wallets[self._config['stake_currency']] = Wallet(
|
_wallets[self._config['stake_currency']] = Wallet(
|
||||||
@ -238,7 +238,7 @@ class Wallets:
|
|||||||
|
|
||||||
return self._check_available_stake_amount(stake_amount, available_amount)
|
return self._check_available_stake_amount(stake_amount, available_amount)
|
||||||
|
|
||||||
def _validate_stake_amount(self, pair, stake_amount, min_stake_amount):
|
def validate_stake_amount(self, pair, stake_amount, min_stake_amount):
|
||||||
if not stake_amount:
|
if not stake_amount:
|
||||||
logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.")
|
logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.")
|
||||||
return 0
|
return 0
|
||||||
@ -250,17 +250,27 @@ class Wallets:
|
|||||||
logger.warning("Minimum stake amount > available balance.")
|
logger.warning("Minimum stake amount > available balance.")
|
||||||
return 0
|
return 0
|
||||||
if min_stake_amount is not None and stake_amount < min_stake_amount:
|
if min_stake_amount is not None and stake_amount < min_stake_amount:
|
||||||
stake_amount = min_stake_amount
|
|
||||||
if self._log:
|
if self._log:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Stake amount for pair {pair} is too small "
|
f"Stake amount for pair {pair} is too small "
|
||||||
f"({stake_amount} < {min_stake_amount}), adjusting to {min_stake_amount}."
|
f"({stake_amount} < {min_stake_amount}), adjusting to {min_stake_amount}."
|
||||||
)
|
)
|
||||||
|
if stake_amount * 1.3 < min_stake_amount:
|
||||||
|
# Top-cap stake-amount adjustments to +30%.
|
||||||
|
if self._log:
|
||||||
|
logger.info(
|
||||||
|
f"Adjusted stake amount for pair {pair} is more than 30% bigger than "
|
||||||
|
f"the desired stake ({stake_amount} * 1.3 > {max_stake_amount}), "
|
||||||
|
f"ignoring trade."
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
stake_amount = min_stake_amount
|
||||||
|
|
||||||
if stake_amount > max_stake_amount:
|
if stake_amount > max_stake_amount:
|
||||||
stake_amount = max_stake_amount
|
|
||||||
if self._log:
|
if self._log:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Stake amount for pair {pair} is too big "
|
f"Stake amount for pair {pair} is too big "
|
||||||
f"({stake_amount} > {max_stake_amount}), adjusting to {max_stake_amount}."
|
f"({stake_amount} > {max_stake_amount}), adjusting to {max_stake_amount}."
|
||||||
)
|
)
|
||||||
|
stake_amount = max_stake_amount
|
||||||
return stake_amount
|
return stake_amount
|
||||||
|
@ -11,8 +11,9 @@ nav:
|
|||||||
- Freqtrade Basics: bot-basics.md
|
- Freqtrade Basics: bot-basics.md
|
||||||
- Configuration: configuration.md
|
- Configuration: configuration.md
|
||||||
- Strategy Customization: strategy-customization.md
|
- Strategy Customization: strategy-customization.md
|
||||||
- Plugins: plugins.md
|
- Strategy Callbacks: strategy-callbacks.md
|
||||||
- Stoploss: stoploss.md
|
- Stoploss: stoploss.md
|
||||||
|
- Plugins: plugins.md
|
||||||
- Start the bot: bot-usage.md
|
- Start the bot: bot-usage.md
|
||||||
- Control the bot:
|
- Control the bot:
|
||||||
- Telegram: telegram-usage.md
|
- Telegram: telegram-usage.md
|
||||||
@ -80,8 +81,10 @@ markdown_extensions:
|
|||||||
- pymdownx.snippets:
|
- pymdownx.snippets:
|
||||||
base_path: docs
|
base_path: docs
|
||||||
check_paths: true
|
check_paths: true
|
||||||
- pymdownx.tabbed
|
|
||||||
- pymdownx.superfences
|
- pymdownx.superfences
|
||||||
|
- pymdownx.tabbed:
|
||||||
|
alternate_style: true
|
||||||
- pymdownx.tasklist:
|
- pymdownx.tasklist:
|
||||||
custom_checkbox: true
|
custom_checkbox: true
|
||||||
|
- pymdownx.tilde
|
||||||
- mdx_truly_sane_lists
|
- mdx_truly_sane_lists
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
-r requirements-plot.txt
|
-r requirements-plot.txt
|
||||||
-r requirements-hyperopt.txt
|
-r requirements-hyperopt.txt
|
||||||
|
|
||||||
coveralls==3.2.0
|
coveralls==3.3.1
|
||||||
flake8==4.0.1
|
flake8==4.0.1
|
||||||
flake8-tidy-imports==4.5.0
|
flake8-tidy-imports==4.5.0
|
||||||
mypy==0.910
|
mypy==0.910
|
||||||
@ -12,15 +12,18 @@ pytest-asyncio==0.16.0
|
|||||||
pytest-cov==3.0.0
|
pytest-cov==3.0.0
|
||||||
pytest-mock==3.6.1
|
pytest-mock==3.6.1
|
||||||
pytest-random-order==1.0.4
|
pytest-random-order==1.0.4
|
||||||
isort==5.9.3
|
isort==5.10.1
|
||||||
# For datetime mocking
|
# For datetime mocking
|
||||||
time-machine==2.4.0
|
time-machine==2.4.1
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==6.2.0
|
nbconvert==6.3.0
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==4.2.4
|
types-cachetools==4.2.6
|
||||||
types-filelock==3.2.1
|
types-filelock==3.2.1
|
||||||
types-requests==2.25.11
|
types-requests==2.26.1
|
||||||
types-tabulate==0.8.3
|
types-tabulate==0.8.3
|
||||||
|
|
||||||
|
# Extensions to datetime library
|
||||||
|
types-python-dateutil==2.8.3
|
@ -2,10 +2,10 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scipy==1.7.1
|
scipy==1.7.3
|
||||||
scikit-learn==1.0
|
scikit-learn==1.0.1
|
||||||
scikit-optimize==0.9.0
|
scikit-optimize==0.9.0
|
||||||
filelock==3.3.1
|
filelock==3.4.0
|
||||||
joblib==1.1.0
|
joblib==1.1.0
|
||||||
psutil==5.8.0
|
psutil==5.8.0
|
||||||
progressbar2==3.55.0
|
progressbar2==3.55.0
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==5.3.1
|
plotly==5.4.0
|
||||||
|
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
numpy==1.21.2
|
numpy==1.21.4
|
||||||
pandas==1.3.4
|
pandas==1.3.4
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.58.47
|
ccxt==1.62.42
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==35.0.0
|
cryptography==36.0.0
|
||||||
aiohttp==3.7.4.post0
|
aiohttp==3.8.1
|
||||||
SQLAlchemy==1.4.25
|
SQLAlchemy==1.4.27
|
||||||
python-telegram-bot==13.7
|
python-telegram-bot==13.8.1
|
||||||
arrow==1.2.0
|
arrow==1.2.1
|
||||||
cachetools==4.2.2
|
cachetools==4.2.2
|
||||||
requests==2.26.0
|
requests==2.26.0
|
||||||
urllib3==1.26.7
|
urllib3==1.26.7
|
||||||
jsonschema==4.1.0
|
jsonschema==4.2.1
|
||||||
TA-Lib==0.4.21
|
TA-Lib==0.4.21
|
||||||
technical==1.3.0
|
technical==1.3.0
|
||||||
tabulate==0.8.9
|
tabulate==0.8.9
|
||||||
pycoingecko==2.2.0
|
pycoingecko==2.2.0
|
||||||
jinja2==3.0.2
|
jinja2==3.0.3
|
||||||
tables==3.6.1
|
tables==3.6.1
|
||||||
blosc==1.10.6
|
blosc==1.10.6
|
||||||
|
|
||||||
@ -34,11 +34,13 @@ sdnotify==0.3.2
|
|||||||
fastapi==0.70.0
|
fastapi==0.70.0
|
||||||
uvicorn==0.15.0
|
uvicorn==0.15.0
|
||||||
pyjwt==2.3.0
|
pyjwt==2.3.0
|
||||||
aiofiles==0.7.0
|
aiofiles==0.8.0
|
||||||
psutil==5.8.0
|
psutil==5.8.0
|
||||||
|
|
||||||
# Support for colorized terminal output
|
# Support for colorized terminal output
|
||||||
colorama==0.4.4
|
colorama==0.4.4
|
||||||
# Building config files interactively
|
# Building config files interactively
|
||||||
questionary==1.10.0
|
questionary==1.10.0
|
||||||
prompt-toolkit==3.0.20
|
prompt-toolkit==3.0.23
|
||||||
|
# Extensions to datetime library
|
||||||
|
python-dateutil==2.8.2
|
||||||
|
@ -39,7 +39,7 @@ class FtRestClient():
|
|||||||
def _call(self, method, apipath, params: dict = None, data=None, files=None):
|
def _call(self, method, apipath, params: dict = None, data=None, files=None):
|
||||||
|
|
||||||
if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'):
|
if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'):
|
||||||
raise ValueError('invalid method <{0}>'.format(method))
|
raise ValueError(f'invalid method <{method}>')
|
||||||
basepath = f"{self._serverurl}/api/v1/{apipath}"
|
basepath = f"{self._serverurl}/api/v1/{apipath}"
|
||||||
|
|
||||||
hd = {"Accept": "application/json",
|
hd = {"Accept": "application/json",
|
||||||
@ -124,7 +124,7 @@ class FtRestClient():
|
|||||||
:param lock_id: ID for the lock to delete
|
:param lock_id: ID for the lock to delete
|
||||||
:return: json object
|
:return: json object
|
||||||
"""
|
"""
|
||||||
return self._delete("locks/{}".format(lock_id))
|
return self._delete(f"locks/{lock_id}")
|
||||||
|
|
||||||
def daily(self, days=None):
|
def daily(self, days=None):
|
||||||
"""Return the profits for each day, and amount of trades.
|
"""Return the profits for each day, and amount of trades.
|
||||||
@ -220,7 +220,7 @@ class FtRestClient():
|
|||||||
:param trade_id: Specify which trade to get.
|
:param trade_id: Specify which trade to get.
|
||||||
:return: json object
|
:return: json object
|
||||||
"""
|
"""
|
||||||
return self._get("trade/{}".format(trade_id))
|
return self._get(f"trade/{trade_id}")
|
||||||
|
|
||||||
def delete_trade(self, trade_id):
|
def delete_trade(self, trade_id):
|
||||||
"""Delete trade from the database.
|
"""Delete trade from the database.
|
||||||
@ -229,7 +229,7 @@ class FtRestClient():
|
|||||||
:param trade_id: Deletes the trade with this ID from the database.
|
:param trade_id: Deletes the trade with this ID from the database.
|
||||||
:return: json object
|
:return: json object
|
||||||
"""
|
"""
|
||||||
return self._delete("trades/{}".format(trade_id))
|
return self._delete(f"trades/{trade_id}")
|
||||||
|
|
||||||
def whitelist(self):
|
def whitelist(self):
|
||||||
"""Show the current whitelist.
|
"""Show the current whitelist.
|
||||||
|
2
setup.py
2
setup.py
@ -43,7 +43,7 @@ setup(
|
|||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
# from requirements.txt
|
# from requirements.txt
|
||||||
'ccxt>=1.50.48',
|
'ccxt>=1.60.11',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy',
|
||||||
'python-telegram-bot>=13.4',
|
'python-telegram-bot>=13.4',
|
||||||
'arrow>=0.17.0',
|
'arrow>=0.17.0',
|
||||||
|
70
setup.sh
70
setup.sh
@ -1,12 +1,16 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#encoding=utf8
|
#encoding=utf8
|
||||||
|
|
||||||
|
function echo_block() {
|
||||||
|
echo "----------------------------"
|
||||||
|
echo $1
|
||||||
|
echo "----------------------------"
|
||||||
|
}
|
||||||
|
|
||||||
function check_installed_pip() {
|
function check_installed_pip() {
|
||||||
${PYTHON} -m pip > /dev/null
|
${PYTHON} -m pip > /dev/null
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "-----------------------------"
|
echo_block "Installing Pip for ${PYTHON}"
|
||||||
echo "Installing Pip for ${PYTHON}"
|
|
||||||
echo "-----------------------------"
|
|
||||||
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
||||||
${PYTHON} get-pip.py
|
${PYTHON} get-pip.py
|
||||||
rm get-pip.py
|
rm get-pip.py
|
||||||
@ -37,9 +41,7 @@ function check_installed_python() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateenv() {
|
function updateenv() {
|
||||||
echo "-------------------------"
|
echo_block "Updating your virtual env"
|
||||||
echo "Updating your virtual env"
|
|
||||||
echo "-------------------------"
|
|
||||||
if [ ! -f .env/bin/activate ]; then
|
if [ ! -f .env/bin/activate ]; then
|
||||||
echo "Something went wrong, no virtual environment found."
|
echo "Something went wrong, no virtual environment found."
|
||||||
exit 1
|
exit 1
|
||||||
@ -110,18 +112,14 @@ function install_mac_newer_python_dependencies() {
|
|||||||
|
|
||||||
if [ ! $(brew --prefix --installed hdf5 2>/dev/null) ]
|
if [ ! $(brew --prefix --installed hdf5 2>/dev/null) ]
|
||||||
then
|
then
|
||||||
echo "-------------------------"
|
echo_block "Installing hdf5"
|
||||||
echo "Installing hdf5"
|
|
||||||
echo "-------------------------"
|
|
||||||
brew install hdf5
|
brew install hdf5
|
||||||
fi
|
fi
|
||||||
export HDF5_DIR=$(brew --prefix)
|
export HDF5_DIR=$(brew --prefix)
|
||||||
|
|
||||||
if [ ! $(brew --prefix --installed c-blosc 2>/dev/null) ]
|
if [ ! $(brew --prefix --installed c-blosc 2>/dev/null) ]
|
||||||
then
|
then
|
||||||
echo "-------------------------"
|
echo_block "Installing c-blosc"
|
||||||
echo "Installing c-blosc"
|
|
||||||
echo "-------------------------"
|
|
||||||
brew install c-blosc
|
brew install c-blosc
|
||||||
fi
|
fi
|
||||||
export CBLOSC_DIR=$(brew --prefix)
|
export CBLOSC_DIR=$(brew --prefix)
|
||||||
@ -131,9 +129,7 @@ function install_mac_newer_python_dependencies() {
|
|||||||
function install_macos() {
|
function install_macos() {
|
||||||
if [ ! -x "$(command -v brew)" ]
|
if [ ! -x "$(command -v brew)" ]
|
||||||
then
|
then
|
||||||
echo "-------------------------"
|
echo_block "Installing Brew"
|
||||||
echo "Installing Brew"
|
|
||||||
echo "-------------------------"
|
|
||||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||||
fi
|
fi
|
||||||
#Gets number after decimal in python version
|
#Gets number after decimal in python version
|
||||||
@ -148,7 +144,14 @@ function install_macos() {
|
|||||||
# Install bot Debian_ubuntu
|
# Install bot Debian_ubuntu
|
||||||
function install_debian() {
|
function install_debian() {
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git $(echo lib${PYTHON}-dev ${PYTHON}-venv)
|
sudo apt-get install -y gcc build-essential autoconf libtool pkg-config make wget git $(echo lib${PYTHON}-dev ${PYTHON}-venv)
|
||||||
|
install_talib
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install bot RedHat_CentOS
|
||||||
|
function install_redhat() {
|
||||||
|
sudo yum update
|
||||||
|
sudo yum install -y gcc gcc-c++ make autoconf libtool pkg-config wget git $(echo ${PYTHON}-devel | sed 's/\.//g')
|
||||||
install_talib
|
install_talib
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,9 +163,7 @@ function update() {
|
|||||||
|
|
||||||
# Reset Develop or Stable branch
|
# Reset Develop or Stable branch
|
||||||
function reset() {
|
function reset() {
|
||||||
echo "----------------------------"
|
echo_block "Resetting branch and virtual env"
|
||||||
echo "Resetting branch and virtual env"
|
|
||||||
echo "----------------------------"
|
|
||||||
|
|
||||||
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* stable") ]
|
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* stable") ]
|
||||||
then
|
then
|
||||||
@ -200,48 +201,39 @@ function reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function config() {
|
function config() {
|
||||||
|
echo_block "Please use 'freqtrade new-config -c config.json' to generate a new configuration file."
|
||||||
echo "-------------------------"
|
|
||||||
echo "Please use 'freqtrade new-config -c config.json' to generate a new configuration file."
|
|
||||||
echo "-------------------------"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function install() {
|
function install() {
|
||||||
echo "-------------------------"
|
|
||||||
echo "Installing mandatory dependencies"
|
echo_block "Installing mandatory dependencies"
|
||||||
echo "-------------------------"
|
|
||||||
|
|
||||||
if [ "$(uname -s)" == "Darwin" ]
|
if [ "$(uname -s)" == "Darwin" ]; then
|
||||||
then
|
|
||||||
echo "macOS detected. Setup for this system in-progress"
|
echo "macOS detected. Setup for this system in-progress"
|
||||||
install_macos
|
install_macos
|
||||||
elif [ -x "$(command -v apt-get)" ]
|
elif [ -x "$(command -v apt-get)" ]; then
|
||||||
then
|
|
||||||
echo "Debian/Ubuntu detected. Setup for this system in-progress"
|
echo "Debian/Ubuntu detected. Setup for this system in-progress"
|
||||||
install_debian
|
install_debian
|
||||||
|
elif [ -x "$(command -v yum)" ]; then
|
||||||
|
echo "Red Hat/CentOS detected. Setup for this system in-progress"
|
||||||
|
install_redhat
|
||||||
else
|
else
|
||||||
echo "This script does not support your OS."
|
echo "This script does not support your OS."
|
||||||
echo "If you have Python3.6 or Python3.7, pip, virtualenv, ta-lib you can continue."
|
echo "If you have Python version 3.7 - 3.9, pip, virtualenv, ta-lib you can continue."
|
||||||
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
|
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
|
||||||
sleep 10
|
sleep 10
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
reset
|
reset
|
||||||
config
|
config
|
||||||
echo "-------------------------"
|
echo_block "Run the bot !"
|
||||||
echo "Run the bot !"
|
|
||||||
echo "-------------------------"
|
|
||||||
echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade <subcommand>'."
|
echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade <subcommand>'."
|
||||||
echo "You can see the list of available bot sub-commands by executing 'source .env/bin/activate; freqtrade --help'."
|
echo "You can see the list of available bot sub-commands by executing 'source .env/bin/activate; freqtrade --help'."
|
||||||
echo "You verify that freqtrade is installed successfully by running 'source .env/bin/activate; freqtrade --version'."
|
echo "You verify that freqtrade is installed successfully by running 'source .env/bin/activate; freqtrade --version'."
|
||||||
}
|
}
|
||||||
|
|
||||||
function plot() {
|
function plot() {
|
||||||
echo "
|
echo_block "Installing dependencies for Plotting scripts"
|
||||||
-----------------------------------------
|
|
||||||
Installing dependencies for Plotting scripts
|
|
||||||
-----------------------------------------
|
|
||||||
"
|
|
||||||
${PYTHON} -m pip install plotly --upgrade
|
${PYTHON} -m pip install plotly --upgrade
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,12 +8,12 @@ from zipfile import ZipFile
|
|||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.commands import (start_convert_data, start_convert_trades, start_create_userdir,
|
from freqtrade.commands import (start_backtesting_show, start_convert_data, start_convert_trades,
|
||||||
start_download_data, start_hyperopt_list, start_hyperopt_show,
|
start_create_userdir, start_download_data, start_hyperopt_list,
|
||||||
start_install_ui, start_list_data, start_list_exchanges,
|
start_hyperopt_show, start_install_ui, start_list_data,
|
||||||
start_list_markets, start_list_strategies, start_list_timeframes,
|
start_list_exchanges, start_list_markets, start_list_strategies,
|
||||||
start_new_strategy, start_show_trades, start_test_pairlist,
|
start_list_timeframes, start_new_strategy, start_show_trades,
|
||||||
start_trading, start_webserver)
|
start_test_pairlist, start_trading, start_webserver)
|
||||||
from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui,
|
from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui,
|
||||||
get_ui_download_url, read_ui_version)
|
get_ui_download_url, read_ui_version)
|
||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
@ -1389,3 +1389,19 @@ def test_show_trades(mocker, fee, capsys, caplog):
|
|||||||
|
|
||||||
with pytest.raises(OperationalException, match=r"--db-url is required for this command."):
|
with pytest.raises(OperationalException, match=r"--db-url is required for this command."):
|
||||||
start_show_trades(pargs)
|
start_show_trades(pargs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtesting_show(mocker, testdatadir, capsys):
|
||||||
|
sbr = mocker.patch('freqtrade.optimize.optimize_reports.show_backtest_results')
|
||||||
|
args = [
|
||||||
|
"backtesting-show",
|
||||||
|
"--export-filename",
|
||||||
|
f"{testdatadir / 'backtest-result_new.json'}",
|
||||||
|
"--show-pair-list"
|
||||||
|
]
|
||||||
|
pargs = get_args(args)
|
||||||
|
pargs['config'] = None
|
||||||
|
start_backtesting_show(pargs)
|
||||||
|
assert sbr.call_count == 1
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert "Pairs for Strategy" in out
|
||||||
|
@ -16,7 +16,7 @@ from telegram import Chat, Message, Update
|
|||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.commands import Arguments
|
from freqtrade.commands import Arguments
|
||||||
from freqtrade.data.converter import ohlcv_to_dataframe
|
from freqtrade.data.converter import ohlcv_to_dataframe
|
||||||
from freqtrade.edge import Edge, PairInfo
|
from freqtrade.edge import PairInfo
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
@ -140,11 +140,6 @@ def patch_edge(mocker) -> None:
|
|||||||
mocker.patch('freqtrade.edge.Edge.calculate', MagicMock(return_value=True))
|
mocker.patch('freqtrade.edge.Edge.calculate', MagicMock(return_value=True))
|
||||||
|
|
||||||
|
|
||||||
def get_patched_edge(mocker, config) -> Edge:
|
|
||||||
patch_edge(mocker)
|
|
||||||
edge = Edge(config)
|
|
||||||
return edge
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
|
|
||||||
|
|
||||||
@ -186,7 +181,7 @@ def get_patched_worker(mocker, config) -> Worker:
|
|||||||
return Worker(args=None, config=config)
|
return Worker(args=None, config=config)
|
||||||
|
|
||||||
|
|
||||||
def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False, None)) -> None:
|
def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False, None, None)) -> None:
|
||||||
"""
|
"""
|
||||||
:param mocker: mocker to patch IStrategy class
|
:param mocker: mocker to patch IStrategy class
|
||||||
:param value: which value IStrategy.get_signal() must return
|
:param value: which value IStrategy.get_signal() must return
|
||||||
@ -2221,6 +2216,46 @@ def market_buy_order_usdt():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def market_buy_order_usdt_doublefee(market_buy_order_usdt):
|
||||||
|
order = deepcopy(market_buy_order_usdt)
|
||||||
|
order['fee'] = None
|
||||||
|
# Market orders filled with 2 trades can have fees in different currencies
|
||||||
|
# assuming the account runs out of BNB.
|
||||||
|
order['fees'] = [
|
||||||
|
{'cost': 0.00025125, 'currency': 'BNB'},
|
||||||
|
{'cost': 0.05030681, 'currency': 'USDT'},
|
||||||
|
]
|
||||||
|
order['trades'] = [{
|
||||||
|
'timestamp': None,
|
||||||
|
'datetime': None,
|
||||||
|
'symbol': 'ETH/USDT',
|
||||||
|
'id': None,
|
||||||
|
'order': '123',
|
||||||
|
'type': 'market',
|
||||||
|
'side': 'sell',
|
||||||
|
'takerOrMaker': None,
|
||||||
|
'price': 2.01,
|
||||||
|
'amount': 25.0,
|
||||||
|
'cost': 50.25,
|
||||||
|
'fee': {'cost': 0.00025125, 'currency': 'BNB'}
|
||||||
|
}, {
|
||||||
|
'timestamp': None,
|
||||||
|
'datetime': None,
|
||||||
|
'symbol': 'ETH/USDT',
|
||||||
|
'id': None,
|
||||||
|
'order': '123',
|
||||||
|
'type': 'market',
|
||||||
|
'side': 'sell',
|
||||||
|
'takerOrMaker': None,
|
||||||
|
'price': 2.0,
|
||||||
|
'amount': 5,
|
||||||
|
'cost': 10,
|
||||||
|
'fee': {'cost': 0.0100306, 'currency': 'USDT'}
|
||||||
|
}]
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def market_sell_order_usdt():
|
def market_sell_order_usdt():
|
||||||
return {
|
return {
|
||||||
|
@ -89,6 +89,7 @@ def mock_trade_2(fee):
|
|||||||
open_order_id='dry_run_sell_12345',
|
open_order_id='dry_run_sell_12345',
|
||||||
strategy='StrategyTestV2',
|
strategy='StrategyTestV2',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
buy_tag='TEST1',
|
||||||
sell_reason='sell_signal',
|
sell_reason='sell_signal',
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||||
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
|
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
|
||||||
@ -241,6 +242,7 @@ def mock_trade_5(fee):
|
|||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
|
buy_tag='TEST1',
|
||||||
stoploss_order_id='prod_stoploss_3455',
|
stoploss_order_id='prod_stoploss_3455',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
)
|
)
|
||||||
@ -295,6 +297,7 @@ def mock_trade_6(fee):
|
|||||||
open_rate=0.15,
|
open_rate=0.15,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
|
buy_tag='TEST2',
|
||||||
open_order_id="prod_sell_6",
|
open_order_id="prod_sell_6",
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
)
|
)
|
||||||
|
@ -126,13 +126,16 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog):
|
|||||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||||
|
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
res = await exchange._async_get_historic_ohlcv(pair, "5m",
|
respair, restf, res = await exchange._async_get_historic_ohlcv(
|
||||||
1500000000000, is_new_pair=False)
|
pair, "5m", 1500000000000, is_new_pair=False)
|
||||||
|
assert respair == pair
|
||||||
|
assert restf == '5m'
|
||||||
# Call with very old timestamp - causes tons of requests
|
# Call with very old timestamp - causes tons of requests
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count > 400
|
assert exchange._api_async.fetch_ohlcv.call_count > 400
|
||||||
# assert res == ohlcv
|
# assert res == ohlcv
|
||||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||||
res = await exchange._async_get_historic_ohlcv(pair, "5m", 1500000000000, is_new_pair=True)
|
_, _, res = await exchange._async_get_historic_ohlcv(
|
||||||
|
pair, "5m", 1500000000000, is_new_pair=True)
|
||||||
|
|
||||||
# Called twice - one "init" call - and one to get the actual data.
|
# Called twice - one "init" call - and one to get the actual data.
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||||
|
@ -47,6 +47,11 @@ EXCHANGES = {
|
|||||||
'hasQuoteVolume': True,
|
'hasQuoteVolume': True,
|
||||||
'timeframe': '5m',
|
'timeframe': '5m',
|
||||||
},
|
},
|
||||||
|
'okex': {
|
||||||
|
'pair': 'BTC/USDT',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '5m',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user