Merge branch 'develop' into pr/dvdmchl/5929
This commit is contained in:
commit
848a2d5383
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -115,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.
|
||||||
|
|
||||||
|
@ -126,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
|
||||||
@ -202,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%).
|
||||||
|
@ -220,6 +220,9 @@ As this Filter uses past performance of the bot, it'll have some startup-period
|
|||||||
|
|
||||||
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:
|
||||||
@ -257,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
|
||||||
|
|
||||||
@ -292,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`.
|
||||||
|
|
||||||
|
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.6
|
mkdocs-material==8.0.1
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==9.1
|
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:
|
||||||
|
|
||||||
|
@ -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.
|
||||||
@ -164,506 +127,6 @@ The provided exit-tag is then used as sell-reason - and shown as such in backtes
|
|||||||
!!! Note
|
!!! Note
|
||||||
`sell_reason` is limited to 100 characters, remaining data will be truncated.
|
`sell_reason` is limited to 100 characters, remaining data will be truncated.
|
||||||
|
|
||||||
## 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')
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Custom stoploss
|
|
||||||
|
|
||||||
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
|
|
||||||
# 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)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 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
|
|
||||||
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.
|
|
||||||
|
|
||||||
!!! 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 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
|
|
||||||
|
|
||||||
### 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
|
||||||
|
|
||||||
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
||||||
|
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
|
||||||
|
|
||||||
|
```
|
@ -317,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.
|
||||||
@ -346,7 +340,7 @@ 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.
|
||||||
|
|
||||||
## Strategy file loading
|
## Strategy file loading
|
||||||
|
|
||||||
@ -1016,6 +1010,10 @@ 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 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.
|
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.
|
||||||
|
@ -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")
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -50,7 +50,7 @@ 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 your 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
|
||||||
|
@ -50,6 +50,8 @@ 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')
|
||||||
@ -312,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'},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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'
|
@ -685,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]:
|
||||||
@ -1263,7 +1267,7 @@ 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_:
|
if raise_:
|
||||||
raise
|
raise
|
||||||
continue
|
continue
|
||||||
@ -1294,7 +1298,7 @@ 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)):
|
||||||
if not since_ms and self.required_candle_call_count > 1:
|
if not since_ms and self.required_candle_call_count > 1:
|
||||||
# Multiple calls for one pair - to get more history
|
# Multiple calls for one pair - to get more history
|
||||||
@ -1317,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)
|
||||||
|
@ -278,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:
|
||||||
@ -286,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):
|
||||||
"""
|
"""
|
||||||
@ -308,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):
|
||||||
"""
|
"""
|
||||||
@ -466,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
|
||||||
@ -510,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,
|
||||||
@ -581,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()
|
||||||
|
|
||||||
@ -593,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'],
|
||||||
@ -644,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
|
||||||
#
|
#
|
||||||
@ -868,7 +852,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info(
|
logger.info(
|
||||||
f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. '
|
f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. '
|
||||||
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
||||||
self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag)
|
self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag=exit_tag)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -1081,7 +1065,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade: Trade,
|
trade: Trade,
|
||||||
limit: float,
|
limit: float,
|
||||||
sell_reason: SellCheckTuple,
|
sell_reason: SellCheckTuple,
|
||||||
exit_tag: Optional[str] = None) -> bool:
|
*,
|
||||||
|
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
|
||||||
@ -1119,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']
|
||||||
@ -1158,16 +1141,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.sell_order_status = ''
|
trade.sell_order_status = ''
|
||||||
trade.close_rate_requested = limit
|
trade.close_rate_requested = limit
|
||||||
trade.sell_reason = exit_tag or 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
|
||||||
|
|
||||||
@ -1264,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:
|
||||||
@ -1310,13 +1294,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
|
@ -67,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']):
|
||||||
@ -89,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.")
|
||||||
|
|
||||||
|
@ -46,20 +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',
|
|
||||||
'Win Draw Loss Win%']
|
|
||||||
|
|
||||||
|
|
||||||
def _get_line_header_sell(first_column: str, stake_currency: str) -> List[str]:
|
|
||||||
"""
|
|
||||||
Generate header lines (goes in line with _generate_result_line())
|
|
||||||
"""
|
|
||||||
return [first_column, 'Sells', '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%']
|
||||||
|
|
||||||
@ -156,7 +147,7 @@ def generate_tag_metrics(tag_type: str,
|
|||||||
if skip_nan and result['profit_abs'].isnull().all():
|
if skip_nan and result['profit_abs'].isnull().all():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tabular_data.append(_generate_tag_result_line(result, starting_balance, tag))
|
tabular_data.append(_generate_result_line(result, starting_balance, tag))
|
||||||
|
|
||||||
# Sort by total profit %:
|
# Sort by total profit %:
|
||||||
tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)
|
tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)
|
||||||
@ -168,39 +159,6 @@ def generate_tag_metrics(tag_type: str,
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _generate_tag_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict:
|
|
||||||
"""
|
|
||||||
Generate one result dict, with "first_column" as key.
|
|
||||||
"""
|
|
||||||
profit_sum = result['profit_ratio'].sum()
|
|
||||||
# (end-capital - starting capital) / starting capital
|
|
||||||
profit_total = result['profit_abs'].sum() / starting_balance
|
|
||||||
|
|
||||||
return {
|
|
||||||
'key': first_column,
|
|
||||||
'trades': len(result),
|
|
||||||
'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0,
|
|
||||||
'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0,
|
|
||||||
'profit_sum': profit_sum,
|
|
||||||
'profit_sum_pct': round(profit_sum * 100.0, 2),
|
|
||||||
'profit_total_abs': result['profit_abs'].sum(),
|
|
||||||
'profit_total': profit_total,
|
|
||||||
'profit_total_pct': round(profit_total * 100.0, 2),
|
|
||||||
'duration_avg': str(timedelta(
|
|
||||||
minutes=round(result['trade_duration'].mean()))
|
|
||||||
) if not result.empty else '0:00',
|
|
||||||
# 'duration_max': str(timedelta(
|
|
||||||
# minutes=round(result['trade_duration'].max()))
|
|
||||||
# ) if not result.empty else '0:00',
|
|
||||||
# 'duration_min': str(timedelta(
|
|
||||||
# minutes=round(result['trade_duration'].min()))
|
|
||||||
# ) if not result.empty else '0:00',
|
|
||||||
'wins': len(result[result['profit_abs'] > 0]),
|
|
||||||
'draws': len(result[result['profit_abs'] == 0]),
|
|
||||||
'losses': len(result[result['profit_abs'] < 0]),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
@ -631,7 +589,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
|
|||||||
if(tag_type == "buy_tag"):
|
if(tag_type == "buy_tag"):
|
||||||
headers = _get_line_header("TAG", stake_currency)
|
headers = _get_line_header("TAG", stake_currency)
|
||||||
else:
|
else:
|
||||||
headers = _get_line_header_sell("TAG", stake_currency)
|
headers = _get_line_header("TAG", stake_currency, 'Sells')
|
||||||
floatfmt = _get_line_floatfmt(stake_currency)
|
floatfmt = _get_line_floatfmt(stake_currency)
|
||||||
output = [
|
output = [
|
||||||
[
|
[
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
@ -125,25 +126,26 @@ class Daily(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class UnfilledTimeout(BaseModel):
|
class UnfilledTimeout(BaseModel):
|
||||||
buy: int
|
buy: Optional[int]
|
||||||
sell: int
|
sell: Optional[int]
|
||||||
unit: str
|
unit: Optional[str]
|
||||||
exit_timeout_count: Optional[int]
|
exit_timeout_count: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class OrderTypes(BaseModel):
|
class OrderTypes(BaseModel):
|
||||||
buy: str
|
buy: OrderTypeValues
|
||||||
sell: str
|
sell: OrderTypeValues
|
||||||
emergencysell: Optional[str]
|
emergencysell: Optional[OrderTypeValues]
|
||||||
forcesell: Optional[str]
|
forcesell: Optional[OrderTypeValues]
|
||||||
forcebuy: Optional[str]
|
forcebuy: Optional[OrderTypeValues]
|
||||||
stoploss: str
|
stoploss: OrderTypeValues
|
||||||
stoploss_on_exchange: bool
|
stoploss_on_exchange: bool
|
||||||
stoploss_on_exchange_interval: Optional[int]
|
stoploss_on_exchange_interval: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class ShowConfig(BaseModel):
|
class ShowConfig(BaseModel):
|
||||||
version: str
|
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]
|
||||||
@ -273,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'])
|
||||||
|
@ -640,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
|
||||||
@ -664,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:
|
||||||
@ -692,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
|
||||||
@ -720,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
|
||||||
|
@ -112,6 +112,7 @@ class Telegram(RPCHandler):
|
|||||||
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]
|
||||||
@ -274,11 +275,11 @@ class Telegram(RPCHandler):
|
|||||||
f"*Buy Tag:* `{msg['buy_tag']}`\n"
|
f"*Buy Tag:* `{msg['buy_tag']}`\n"
|
||||||
f"*Sell Reason:* `{msg['sell_reason']}`\n"
|
f"*Sell Reason:* `{msg['sell_reason']}`\n"
|
||||||
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
|
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
|
||||||
f"*Amount:* `{msg['amount']:.8f}`\n")
|
f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||||
|
f"*Open Rate:* `{msg['open_rate']:.8f}`\n")
|
||||||
|
|
||||||
if msg['type'] == RPCMessageType.SELL:
|
if msg['type'] == RPCMessageType.SELL:
|
||||||
message += (f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
|
message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
||||||
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
|
||||||
f"*Close Rate:* `{msg['limit']:.8f}`")
|
f"*Close Rate:* `{msg['limit']:.8f}`")
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.SELL_FILL:
|
elif msg['type'] == RPCMessageType.SELL_FILL:
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -14,16 +14,16 @@ pytest-mock==3.6.1
|
|||||||
pytest-random-order==1.0.4
|
pytest-random-order==1.0.4
|
||||||
isort==5.10.1
|
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.3.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.26.0
|
types-requests==2.26.1
|
||||||
types-tabulate==0.8.3
|
types-tabulate==0.8.3
|
||||||
|
|
||||||
# Extensions to datetime library
|
# Extensions to datetime library
|
||||||
types-python-dateutil==2.8.2
|
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.2
|
scipy==1.7.3
|
||||||
scikit-learn==1.0.1
|
scikit-learn==1.0.1
|
||||||
scikit-optimize==0.9.0
|
scikit-optimize==0.9.0
|
||||||
filelock==3.3.2
|
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
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@ numpy==1.21.4
|
|||||||
pandas==1.3.4
|
pandas==1.3.4
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.61.24
|
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.27
|
SQLAlchemy==1.4.27
|
||||||
python-telegram-bot==13.8.1
|
python-telegram-bot==13.8.1
|
||||||
arrow==1.2.1
|
arrow==1.2.1
|
||||||
@ -34,13 +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.22
|
prompt-toolkit==3.0.23
|
||||||
# Extensions to datetime library
|
# Extensions to datetime library
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
|
@ -1026,6 +1026,12 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice,
|
|||||||
assert order_closed['status'] == 'closed'
|
assert order_closed['status'] == 'closed'
|
||||||
assert order['fee']
|
assert order['fee']
|
||||||
|
|
||||||
|
# Empty orderbook test
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book',
|
||||||
|
return_value={'asks': [], 'bids': []})
|
||||||
|
exchange._dry_run_open_orders[order['id']]['status'] = 'open'
|
||||||
|
order_closed = exchange.fetch_dry_run_order(order['id'])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("side,rate,amount,endprice", [
|
@pytest.mark.parametrize("side,rate,amount,endprice", [
|
||||||
# spread is 25.263-25.266
|
# spread is 25.263-25.266
|
||||||
@ -1667,12 +1673,21 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
|||||||
assert len(res) == len(pairs)
|
assert len(res) == len(pairs)
|
||||||
|
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
||||||
|
exchange.required_candle_call_count = 1
|
||||||
assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, "
|
assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, "
|
||||||
f"timeframe {pairs[0][1]} ...",
|
f"timeframe {pairs[0][1]} ...",
|
||||||
caplog)
|
caplog)
|
||||||
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')],
|
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')],
|
||||||
cache=False)
|
cache=False)
|
||||||
assert len(res) == 3
|
assert len(res) == 3
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 3
|
||||||
|
|
||||||
|
# Test the same again, should NOT return from cache!
|
||||||
|
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||||
|
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')],
|
||||||
|
cache=False)
|
||||||
|
assert len(res) == 3
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -1768,7 +1783,7 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
|
|||||||
assert len(res) == 1
|
assert len(res) == 1
|
||||||
# Test that each is in list at least once as order is not guaranteed
|
# Test that each is in list at least once as order is not guaranteed
|
||||||
assert log_has("Error loading ETH/BTC. Result was [[]].", caplog)
|
assert log_has("Error loading ETH/BTC. Result was [[]].", caplog)
|
||||||
assert log_has("Async code raised an exception: TypeError", caplog)
|
assert log_has("Async code raised an exception: TypeError()", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_get_next_limit_in_list():
|
def test_get_next_limit_in_list():
|
||||||
|
@ -438,7 +438,8 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) ->
|
|||||||
Backtesting(default_conf)
|
Backtesting(default_conf)
|
||||||
|
|
||||||
default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}]
|
default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}]
|
||||||
with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'):
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'VolumePairList not allowed for backtesting\..*StaticPairlist.*'):
|
||||||
Backtesting(default_conf)
|
Backtesting(default_conf)
|
||||||
|
|
||||||
default_conf.update({
|
default_conf.update({
|
||||||
@ -470,7 +471,8 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
|
|||||||
default_conf['timerange'] = '20180101-20180102'
|
default_conf['timerange'] = '20180101-20180102'
|
||||||
|
|
||||||
default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}]
|
default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}]
|
||||||
with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'):
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'VolumePairList not allowed for backtesting\..*StaticPairlist.*'):
|
||||||
Backtesting(default_conf)
|
Backtesting(default_conf)
|
||||||
|
|
||||||
default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}]
|
default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}]
|
||||||
|
@ -7,6 +7,7 @@ import pytest
|
|||||||
import time_machine
|
import time_machine
|
||||||
|
|
||||||
from freqtrade.constants import AVAILABLE_PAIRLISTS
|
from freqtrade.constants import AVAILABLE_PAIRLISTS
|
||||||
|
from freqtrade.enums.runmode import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
@ -657,6 +658,22 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
|
|||||||
assert log_has("PerformanceFilter is not available in this mode.", caplog)
|
assert log_has("PerformanceFilter is not available in this mode.", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None:
|
||||||
|
whitelist_conf['pairlists'] = [
|
||||||
|
{"method": "StaticPairList"},
|
||||||
|
{"method": "ShuffleFilter", "seed": 42}
|
||||||
|
]
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, whitelist_conf)
|
||||||
|
PairListManager(exchange, whitelist_conf)
|
||||||
|
assert log_has("Backtesting mode detected, applying seed value: 42", caplog)
|
||||||
|
caplog.clear()
|
||||||
|
whitelist_conf['runmode'] = RunMode.DRY_RUN
|
||||||
|
PairListManager(exchange, whitelist_conf)
|
||||||
|
assert not log_has("Backtesting mode detected, applying seed value: 42", caplog)
|
||||||
|
assert log_has("Live mode detected, not applying seed.", caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None:
|
def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None:
|
||||||
whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC')
|
whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC')
|
||||||
|
@ -1093,7 +1093,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) ->
|
|||||||
with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'):
|
with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'):
|
||||||
rpc._rpc_forcebuy(pair, 0.0001)
|
rpc._rpc_forcebuy(pair, 0.0001)
|
||||||
pair = 'XRP/BTC'
|
pair = 'XRP/BTC'
|
||||||
trade = rpc._rpc_forcebuy(pair, 0.0001)
|
trade = rpc._rpc_forcebuy(pair, 0.0001, order_type='limit')
|
||||||
assert isinstance(trade, Trade)
|
assert isinstance(trade, Trade)
|
||||||
assert trade.pair == pair
|
assert trade.pair == pair
|
||||||
assert trade.open_rate == 0.0001
|
assert trade.open_rate == 0.0001
|
||||||
|
@ -538,6 +538,8 @@ def test_api_show_config(botclient):
|
|||||||
assert 'ask_strategy' in rc.json()
|
assert 'ask_strategy' in rc.json()
|
||||||
assert 'unfilledtimeout' in rc.json()
|
assert 'unfilledtimeout' in rc.json()
|
||||||
assert 'version' in rc.json()
|
assert 'version' in rc.json()
|
||||||
|
assert 'api_version' in rc.json()
|
||||||
|
assert 1.1 <= rc.json()['api_version'] <= 1.2
|
||||||
|
|
||||||
|
|
||||||
def test_api_daily(botclient, mocker, ticker, fee, markets):
|
def test_api_daily(botclient, mocker, ticker, fee, markets):
|
||||||
|
@ -24,6 +24,7 @@ from freqtrade.freqtradebot import FreqtradeBot
|
|||||||
from freqtrade.loggers import setup_logging
|
from freqtrade.loggers import setup_logging
|
||||||
from freqtrade.persistence import PairLocks, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
from freqtrade.rpc import RPC
|
from freqtrade.rpc import RPC
|
||||||
|
from freqtrade.rpc.rpc import RPCException
|
||||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||||
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re,
|
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re,
|
||||||
patch_exchange, patch_get_signal, patch_whitelist)
|
patch_exchange, patch_get_signal, patch_whitelist)
|
||||||
@ -936,7 +937,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
telegram._forcesell(update=update, context=context)
|
telegram._forcesell(update=update, context=context)
|
||||||
|
|
||||||
assert msg_mock.call_count == 4
|
assert msg_mock.call_count == 4
|
||||||
last_msg = msg_mock.call_args_list[-1][0][0]
|
last_msg = msg_mock.call_args_list[-2][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
@ -1000,7 +1001,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
|
|||||||
|
|
||||||
assert msg_mock.call_count == 4
|
assert msg_mock.call_count == 4
|
||||||
|
|
||||||
last_msg = msg_mock.call_args_list[-1][0][0]
|
last_msg = msg_mock.call_args_list[-2][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
@ -1054,7 +1055,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
|||||||
|
|
||||||
# Called for each trade 2 times
|
# Called for each trade 2 times
|
||||||
assert msg_mock.call_count == 8
|
assert msg_mock.call_count == 8
|
||||||
msg = msg_mock.call_args_list[1][0][0]
|
msg = msg_mock.call_args_list[0][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
@ -1186,8 +1187,8 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None:
|
|||||||
assert fbuy_mock.call_count == 1
|
assert fbuy_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_performance_handle(default_conf, update, ticker, fee,
|
def test_telegram_performance_handle(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -1216,8 +1217,8 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
|||||||
assert '<code>ETH/BTC\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
assert '<code>ETH/BTC\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_buy_tag_performance_handle(default_conf, update, ticker, fee,
|
def test_telegram_buy_tag_performance_handle(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
@ -1240,15 +1241,27 @@ def test_buy_tag_performance_handle(default_conf, update, ticker, fee,
|
|||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
|
context = MagicMock()
|
||||||
telegram._buy_tag_performance(update=update, context=MagicMock())
|
telegram._buy_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Buy Tag Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Buy Tag Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>TESTBUY\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
assert '<code>TESTBUY\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
context.args = [trade.pair]
|
||||||
|
telegram._buy_tag_performance(update=update, context=context)
|
||||||
|
assert msg_mock.call_count == 2
|
||||||
|
|
||||||
def test_sell_reason_performance_handle(default_conf, update, ticker, fee,
|
msg_mock.reset_mock()
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_buy_tag_performance',
|
||||||
|
side_effect=RPCException('Error'))
|
||||||
|
telegram._buy_tag_performance(update=update, context=MagicMock())
|
||||||
|
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert "Error" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_telegram_sell_reason_performance_handle(default_conf, update, ticker, fee,
|
||||||
|
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
@ -1271,15 +1284,27 @@ def test_sell_reason_performance_handle(default_conf, update, ticker, fee,
|
|||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
|
context = MagicMock()
|
||||||
telegram._sell_reason_performance(update=update, context=MagicMock())
|
telegram._sell_reason_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Sell Reason Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Sell Reason Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>TESTSELL\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
assert '<code>TESTSELL\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
context.args = [trade.pair]
|
||||||
|
|
||||||
|
telegram._sell_reason_performance(update=update, context=context)
|
||||||
|
assert msg_mock.call_count == 2
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_sell_reason_performance',
|
||||||
|
side_effect=RPCException('Error'))
|
||||||
|
telegram._sell_reason_performance(update=update, context=MagicMock())
|
||||||
|
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert "Error" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_mix_tag_performance_handle(default_conf, update, ticker, fee,
|
def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
@ -1305,12 +1330,25 @@ def test_mix_tag_performance_handle(default_conf, update, ticker, fee,
|
|||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
|
|
||||||
telegram._mix_tag_performance(update=update, context=MagicMock())
|
context = MagicMock()
|
||||||
|
telegram._mix_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert ('<code>TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)</code>'
|
assert ('<code>TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)</code>'
|
||||||
in msg_mock.call_args_list[0][0][0])
|
in msg_mock.call_args_list[0][0][0])
|
||||||
|
|
||||||
|
context.args = [trade.pair]
|
||||||
|
telegram._mix_tag_performance(update=update, context=context)
|
||||||
|
assert msg_mock.call_count == 2
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_mix_tag_performance',
|
||||||
|
side_effect=RPCException('Error'))
|
||||||
|
telegram._mix_tag_performance(update=update, context=MagicMock())
|
||||||
|
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert "Error" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -1851,6 +1889,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None:
|
|||||||
'*Sell Reason:* `stop_loss`\n'
|
'*Sell Reason:* `stop_loss`\n'
|
||||||
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
|
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Close Rate:* `0.00003201`'
|
'*Close Rate:* `0.00003201`'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -292,3 +292,15 @@ def test__send_msg_with_json_format(default_conf, mocker, caplog):
|
|||||||
webhook._send_msg(msg)
|
webhook._send_msg(msg)
|
||||||
|
|
||||||
assert post.call_args[1] == {'json': msg}
|
assert post.call_args[1] == {'json': msg}
|
||||||
|
|
||||||
|
|
||||||
|
def test__send_msg_with_raw_format(default_conf, mocker, caplog):
|
||||||
|
default_conf["webhook"] = get_webhook_dict()
|
||||||
|
default_conf["webhook"]["format"] = "raw"
|
||||||
|
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
|
||||||
|
msg = {'data': 'Hello'}
|
||||||
|
post = MagicMock()
|
||||||
|
mocker.patch("freqtrade.rpc.webhook.post", post)
|
||||||
|
webhook._send_msg(msg)
|
||||||
|
|
||||||
|
assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}}
|
||||||
|
@ -20,7 +20,7 @@ class InformativeDecoratorTest(IStrategy):
|
|||||||
startup_candle_count: int = 20
|
startup_candle_count: int = 20
|
||||||
|
|
||||||
def informative_pairs(self):
|
def informative_pairs(self):
|
||||||
return [('BTC/USDT', '5m')]
|
return [('NEO/USDT', '5m')]
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
dataframe['buy'] = 0
|
dataframe['buy'] = 0
|
||||||
@ -38,8 +38,8 @@ class InformativeDecoratorTest(IStrategy):
|
|||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
# Simple informative test.
|
# Simple informative test.
|
||||||
@informative('1h', 'BTC/{stake}')
|
@informative('1h', 'NEO/{stake}')
|
||||||
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators_neo_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
dataframe['rsi'] = 14
|
dataframe['rsi'] = 14
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ class InformativeDecoratorTest(IStrategy):
|
|||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
# Formatting test.
|
# Formatting test.
|
||||||
@informative('30m', 'BTC/{stake}', '{column}_{BASE}_{QUOTE}_{base}_{quote}_{asset}_{timeframe}')
|
@informative('30m', 'NEO/{stake}', '{column}_{BASE}_{QUOTE}_{base}_{quote}_{asset}_{timeframe}')
|
||||||
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
dataframe['rsi'] = 14
|
dataframe['rsi'] = 14
|
||||||
return dataframe
|
return dataframe
|
||||||
@ -68,7 +68,7 @@ class InformativeDecoratorTest(IStrategy):
|
|||||||
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
||||||
|
|
||||||
# Mixing manual informative pairs with decorators.
|
# Mixing manual informative pairs with decorators.
|
||||||
informative = self.dp.get_pair_dataframe('BTC/USDT', '5m')
|
informative = self.dp.get_pair_dataframe('NEO/USDT', '5m')
|
||||||
informative['rsi'] = 14
|
informative['rsi'] = 14
|
||||||
dataframe = merge_informative_pair(dataframe, informative, self.timeframe, '5m', ffill=True)
|
dataframe = merge_informative_pair(dataframe, informative, self.timeframe, '5m', ffill=True)
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import pytest
|
|||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open,
|
from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open,
|
||||||
timeframe_to_minutes)
|
timeframe_to_minutes)
|
||||||
|
from tests.conftest import get_patched_exchange
|
||||||
|
|
||||||
|
|
||||||
def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'):
|
def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'):
|
||||||
@ -155,9 +156,9 @@ def test_informative_decorator(mocker, default_conf):
|
|||||||
('LTC/USDT', '5m'): test_data_5m,
|
('LTC/USDT', '5m'): test_data_5m,
|
||||||
('LTC/USDT', '30m'): test_data_30m,
|
('LTC/USDT', '30m'): test_data_30m,
|
||||||
('LTC/USDT', '1h'): test_data_1h,
|
('LTC/USDT', '1h'): test_data_1h,
|
||||||
('BTC/USDT', '30m'): test_data_30m,
|
('NEO/USDT', '30m'): test_data_30m,
|
||||||
('BTC/USDT', '5m'): test_data_5m,
|
('NEO/USDT', '5m'): test_data_5m,
|
||||||
('BTC/USDT', '1h'): test_data_1h,
|
('NEO/USDT', '1h'): test_data_1h,
|
||||||
('ETH/USDT', '1h'): test_data_1h,
|
('ETH/USDT', '1h'): test_data_1h,
|
||||||
('ETH/USDT', '30m'): test_data_30m,
|
('ETH/USDT', '30m'): test_data_30m,
|
||||||
('ETH/BTC', '1h'): test_data_1h,
|
('ETH/BTC', '1h'): test_data_1h,
|
||||||
@ -165,15 +166,16 @@ def test_informative_decorator(mocker, default_conf):
|
|||||||
from .strats.informative_decorator_strategy import InformativeDecoratorTest
|
from .strats.informative_decorator_strategy import InformativeDecoratorTest
|
||||||
default_conf['stake_currency'] = 'USDT'
|
default_conf['stake_currency'] = 'USDT'
|
||||||
strategy = InformativeDecoratorTest(config=default_conf)
|
strategy = InformativeDecoratorTest(config=default_conf)
|
||||||
strategy.dp = DataProvider({}, None, None)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
strategy.dp = DataProvider({}, exchange, None)
|
||||||
mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[
|
mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[
|
||||||
'XRP/USDT', 'LTC/USDT', 'BTC/USDT'
|
'XRP/USDT', 'LTC/USDT', 'NEO/USDT'
|
||||||
])
|
])
|
||||||
|
|
||||||
assert len(strategy._ft_informative) == 6 # Equal to number of decorators used
|
assert len(strategy._ft_informative) == 6 # Equal to number of decorators used
|
||||||
informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'),
|
informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'),
|
||||||
('LTC/USDT', '30m'), ('BTC/USDT', '1h'), ('BTC/USDT', '30m'),
|
('LTC/USDT', '30m'), ('NEO/USDT', '1h'), ('NEO/USDT', '30m'),
|
||||||
('BTC/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')]
|
('NEO/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')]
|
||||||
for inf_pair in informative_pairs:
|
for inf_pair in informative_pairs:
|
||||||
assert inf_pair in strategy.gather_informative_pairs()
|
assert inf_pair in strategy.gather_informative_pairs()
|
||||||
|
|
||||||
@ -186,8 +188,8 @@ def test_informative_decorator(mocker, default_conf):
|
|||||||
{p: data[(p, strategy.timeframe)] for p in ('XRP/USDT', 'LTC/USDT')})
|
{p: data[(p, strategy.timeframe)] for p in ('XRP/USDT', 'LTC/USDT')})
|
||||||
expected_columns = [
|
expected_columns = [
|
||||||
'rsi_1h', 'rsi_30m', # Stacked informative decorators
|
'rsi_1h', 'rsi_30m', # Stacked informative decorators
|
||||||
'btc_usdt_rsi_1h', # BTC 1h informative
|
'neo_usdt_rsi_1h', # NEO 1h informative
|
||||||
'rsi_BTC_USDT_btc_usdt_BTC/USDT_30m', # Column formatting
|
'rsi_NEO_USDT_neo_usdt_NEO/USDT_30m', # Column formatting
|
||||||
'rsi_from_callable', # Custom column formatter
|
'rsi_from_callable', # Custom column formatter
|
||||||
'eth_btc_rsi_1h', # Quote currency not matching stake currency
|
'eth_btc_rsi_1h', # Quote currency not matching stake currency
|
||||||
'rsi', 'rsi_less', # Non-informative columns
|
'rsi', 'rsi_less', # Non-informative columns
|
||||||
|
@ -2979,7 +2979,7 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee,
|
|||||||
assert trade.close_profit == 0.09451372
|
assert trade.close_profit == 0.09451372
|
||||||
|
|
||||||
assert rpc_mock.call_count == 3
|
assert rpc_mock.call_count == 3
|
||||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-2][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
|
@ -67,6 +67,9 @@ def test_file_load_json(mocker, testdatadir) -> None:
|
|||||||
|
|
||||||
@pytest.mark.parametrize("pair,expected_result", [
|
@pytest.mark.parametrize("pair,expected_result", [
|
||||||
("ETH/BTC", 'ETH_BTC'),
|
("ETH/BTC", 'ETH_BTC'),
|
||||||
|
("ETH/USDT", 'ETH_USDT'),
|
||||||
|
("ETH/USDT:USDT", 'ETH_USDT_USDT'), # swap with USDT as settlement currency
|
||||||
|
("ETH/USDT:USDT-210625", 'ETH_USDT_USDT_210625'), # expiring futures
|
||||||
("Fabric Token/ETH", 'Fabric_Token_ETH'),
|
("Fabric Token/ETH", 'Fabric_Token_ETH'),
|
||||||
("ETHH20", 'ETHH20'),
|
("ETHH20", 'ETHH20'),
|
||||||
(".XBTBON2H", '_XBTBON2H'),
|
(".XBTBON2H", '_XBTBON2H'),
|
||||||
|
Loading…
Reference in New Issue
Block a user