Merge branch 'feat/short' into pr/samgermain/5780
This commit is contained in:
commit
f4d0abc51a
@ -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.
|
||||||
|
|
||||||
|
@ -43,3 +43,24 @@ As this does however increase risk and provides no benefit, it's been removed fo
|
|||||||
|
|
||||||
Using separate hyperopt files was deprecated in 2021.4 and was removed in 2021.9.
|
Using separate hyperopt files was deprecated in 2021.4 and was removed in 2021.9.
|
||||||
Please switch to the new [Parametrized Strategies](hyperopt.md) to benefit from the new hyperopt interface.
|
Please switch to the new [Parametrized Strategies](hyperopt.md) to benefit from the new hyperopt interface.
|
||||||
|
|
||||||
|
## Margin / short changes
|
||||||
|
|
||||||
|
// TODO-lev: update version here
|
||||||
|
|
||||||
|
## Strategy changes
|
||||||
|
|
||||||
|
As strategies now have to support multiple different signal types, some things had to change.
|
||||||
|
|
||||||
|
Columns:
|
||||||
|
|
||||||
|
* `buy` -> `enter_long`
|
||||||
|
* `sell` -> `exit_long`
|
||||||
|
* `buy_tag` -> `enter_tag`
|
||||||
|
|
||||||
|
New columns are `enter_short` and `exit_short`, which will initiate short trades (requires additional configuration!)
|
||||||
|
|
||||||
|
### webhooks - `buy_tag` has been renamed to `enter_tag`
|
||||||
|
|
||||||
|
This should apply only to your strategy and potentially to webhooks.
|
||||||
|
We will keep a compatibility layer for 1-2 versions (so both `buy_tag` and `enter_tag` will still work), but support for this in webhooks will disappear after that.
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -164,7 +164,7 @@ 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
|
||||||
@ -174,6 +174,7 @@ The sample plot configuration below specifies fixed colors for the indicators. O
|
|||||||
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,6 +183,54 @@ 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
|
||||||
|
@property
|
||||||
|
def plot_config(self):
|
||||||
|
"""
|
||||||
|
There are a lot of solutions how to build the return dictionary.
|
||||||
|
The only important point is the return value.
|
||||||
|
Example:
|
||||||
|
plot_config = {'main_plot': {}, 'subplots': {}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
plot_config = {}
|
||||||
|
plot_config['main_plot'] = {
|
||||||
|
# Configuration for main plot indicators.
|
||||||
|
# Assumes 2 parameters, emashort and emalong to be specified.
|
||||||
|
f'ema_{self.emashort.value}': {'color': 'red'},
|
||||||
|
f'ema_{self.emalong.value}': {'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': {}
|
||||||
|
}
|
||||||
|
plot_config['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'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {
|
plot_config = {
|
||||||
'main_plot': {
|
'main_plot': {
|
||||||
# Configuration for main plot indicators.
|
# Configuration for main plot indicators.
|
||||||
@ -214,7 +263,8 @@ Sample configuration with inline comments explaining the process:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
|
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
|
||||||
|
@ -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,7 +77,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
## Buy Tag
|
## Enter 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.
|
||||||
Then you can access you buy signal on `custom_sell`
|
Then you can access you buy signal on `custom_sell`
|
||||||
@ -89,7 +89,7 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|||||||
(dataframe['rsi'] < 35) &
|
(dataframe['rsi'] < 35) &
|
||||||
(dataframe['volume'] > 0)
|
(dataframe['volume'] > 0)
|
||||||
),
|
),
|
||||||
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
|
['buy', 'enter_tag']] = (1, 'buy_signal_rsi')
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
@ -97,14 +97,14 @@ def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_r
|
|||||||
current_profit: float, **kwargs):
|
current_profit: float, **kwargs):
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
last_candle = dataframe.iloc[-1].squeeze()
|
last_candle = dataframe.iloc[-1].squeeze()
|
||||||
if trade.buy_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80:
|
if trade.enter_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80:
|
||||||
return 'sell_signal_rsi'
|
return 'sell_signal_rsi'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
`buy_tag` is limited to 100 characters, remaining data will be truncated.
|
`enter_tag` is limited to 100 characters, remaining data will be truncated.
|
||||||
|
|
||||||
## Exit tag
|
## Exit tag
|
||||||
|
|
||||||
|
@ -498,7 +498,7 @@ for more information.
|
|||||||
&
|
&
|
||||||
(dataframe['volume'] > 0)
|
(dataframe['volume'] > 0)
|
||||||
),
|
),
|
||||||
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
|
['buy', 'enter_tag']] = (1, 'buy_signal_rsi')
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
@ -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`
|
||||||
@ -83,7 +113,7 @@ Possible parameters are:
|
|||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
* `buy_tag`
|
* `enter_tag`
|
||||||
|
|
||||||
### Webhookbuycancel
|
### Webhookbuycancel
|
||||||
|
|
||||||
@ -101,7 +131,7 @@ Possible parameters are:
|
|||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
* `buy_tag`
|
* `enter_tag`
|
||||||
|
|
||||||
### Webhookbuyfill
|
### Webhookbuyfill
|
||||||
|
|
||||||
@ -117,7 +147,9 @@ Possible parameters are:
|
|||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `buy_tag`
|
* `order_type`
|
||||||
|
* `current_rate`
|
||||||
|
* `enter_tag`
|
||||||
|
|
||||||
### Webhooksell
|
### Webhooksell
|
||||||
|
|
||||||
|
@ -52,6 +52,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')
|
||||||
@ -316,10 +318,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'},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -30,7 +30,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
|
|||||||
'fee_open', 'fee_close', 'trade_duration',
|
'fee_open', 'fee_close', 'trade_duration',
|
||||||
'profit_ratio', 'profit_abs', 'sell_reason',
|
'profit_ratio', 'profit_abs', 'sell_reason',
|
||||||
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
||||||
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag',
|
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag',
|
||||||
'is_short'
|
'is_short'
|
||||||
]
|
]
|
||||||
# TODO-lev: usage of the above might need compatibility code (buy_tag, is_short?, ...?)
|
# TODO-lev: usage of the above might need compatibility code (buy_tag, is_short?, ...?)
|
||||||
|
@ -77,10 +77,10 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
|
|
||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
|
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
|
||||||
|
|
||||||
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, candle_type: str = '') -> pd.DataFrame:
|
timerange: Optional[TimeRange] = None, candle_type: str = '') -> pd.DataFrame:
|
||||||
@ -172,11 +172,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):
|
||||||
"""
|
"""
|
||||||
|
@ -23,6 +23,7 @@ BAD_EXCHANGES = {
|
|||||||
MAP_EXCHANGE_CHILDCLASS = {
|
MAP_EXCHANGE_CHILDCLASS = {
|
||||||
'binanceus': 'binance',
|
'binanceus': 'binance',
|
||||||
'binanceje': 'binance',
|
'binanceje': 'binance',
|
||||||
|
'binanceusdm': 'binance',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1372,7 +1372,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
|
||||||
@ -1405,7 +1405,7 @@ class Exchange:
|
|||||||
cached_pairs = []
|
cached_pairs = []
|
||||||
# Gather coroutines to run
|
# Gather coroutines to run
|
||||||
for pair, timeframe, candle_type in set(pair_list):
|
for pair, timeframe, candle_type in set(pair_list):
|
||||||
if ((pair, timeframe, candle_type) not in self._klines
|
if ((pair, timeframe, candle_type) 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
|
||||||
@ -1428,16 +1428,17 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
cached_pairs.append((pair, timeframe, candle_type))
|
cached_pairs.append((pair, timeframe, candle_type))
|
||||||
|
|
||||||
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 input_coro in chunks(input_coroutines, 100):
|
||||||
|
results = asyncio.get_event_loop().run_until_complete(
|
||||||
|
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)}")
|
||||||
continue
|
continue
|
||||||
# Deconstruct tuple (has 3 elements)
|
# Deconstruct tuple (has 4 elements)
|
||||||
pair, timeframe, c_type, ticks = res
|
pair, timeframe, c_type, ticks = res
|
||||||
# keeping last candle time as last refreshed time of the pair
|
# keeping last candle time as last refreshed time of the pair
|
||||||
if ticks:
|
if ticks:
|
||||||
|
@ -322,7 +322,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
f"for order {order.order_id}."
|
f"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:
|
||||||
@ -333,7 +334,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
f"Updating {trade.enter_side}-fee on trade {trade}"
|
f"Updating {trade.enter_side}-fee on trade {trade}"
|
||||||
f"for order {order.order_id}."
|
f"for order {order.order_id}."
|
||||||
)
|
)
|
||||||
self.update_trade_state(trade, order.order_id)
|
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):
|
||||||
"""
|
"""
|
||||||
@ -356,7 +357,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if order:
|
if order:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.")
|
f"Updating {trade.enter_side}-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):
|
||||||
"""
|
"""
|
||||||
@ -575,8 +576,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
pair: str,
|
pair: str,
|
||||||
stake_amount: float,
|
stake_amount: float,
|
||||||
price: Optional[float] = None,
|
price: Optional[float] = None,
|
||||||
forcebuy: bool = False,
|
*,
|
||||||
is_short: bool = False,
|
is_short: bool = False,
|
||||||
|
ordertype: Optional[str] = None,
|
||||||
enter_tag: Optional[str] = None
|
enter_tag: Optional[str] = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -649,12 +651,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
amount = (stake_amount / enter_limit_requested) * leverage
|
amount = (stake_amount / enter_limit_requested) * leverage
|
||||||
order_type = self.strategy.order_types['buy']
|
order_type = ordertype or self.strategy.order_types['buy']
|
||||||
if forcebuy:
|
|
||||||
# Forcebuy can define a different ordertype
|
|
||||||
# TODO-lev: get a forceshort? What is this
|
|
||||||
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
|
||||||
# TODO-lev: Will this work for shorting?
|
|
||||||
|
|
||||||
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,
|
||||||
@ -736,8 +733,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
exchange=self.exchange.id,
|
exchange=self.exchange.id,
|
||||||
open_order_id=order_id,
|
open_order_id=order_id,
|
||||||
strategy=self.strategy.get_strategy_name(),
|
strategy=self.strategy.get_strategy_name(),
|
||||||
# TODO-lev: compatibility layer for buy_tag (!)
|
enter_tag=enter_tag,
|
||||||
buy_tag=enter_tag,
|
|
||||||
timeframe=timeframe_to_minutes(self.config['timeframe']),
|
timeframe=timeframe_to_minutes(self.config['timeframe']),
|
||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
is_short=is_short,
|
is_short=is_short,
|
||||||
@ -748,10 +744,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()
|
||||||
|
|
||||||
@ -760,19 +752,31 @@ 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 entry order occurred.
|
Sends rpc notification when a entry order occurred.
|
||||||
"""
|
"""
|
||||||
|
if fill:
|
||||||
|
msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL
|
||||||
|
else:
|
||||||
|
msg_type = RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY
|
||||||
|
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY,
|
'type': msg_type,
|
||||||
'buy_tag': trade.buy_tag,
|
'buy_tag': trade.enter_tag,
|
||||||
|
'enter_tag': trade.enter_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'],
|
||||||
@ -794,7 +798,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': msg_type,
|
'type': msg_type,
|
||||||
'buy_tag': trade.buy_tag,
|
'buy_tag': trade.enter_tag,
|
||||||
|
'enter_tag': trade.enter_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,
|
||||||
@ -811,23 +816,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_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL
|
|
||||||
msg = {
|
|
||||||
'trade_id': trade.id,
|
|
||||||
'type': msg_type,
|
|
||||||
'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
|
||||||
#
|
#
|
||||||
@ -1051,7 +1039,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if should_exit.sell_flag:
|
if should_exit.sell_flag:
|
||||||
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}'
|
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.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_exit, exit_tag)
|
self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -1272,8 +1260,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self,
|
self,
|
||||||
trade: Trade,
|
trade: Trade,
|
||||||
limit: float,
|
limit: float,
|
||||||
sell_reason: SellCheckTuple, # TODO-lev update to exit_reason
|
sell_reason: SellCheckTuple,
|
||||||
exit_tag: Optional[str] = None
|
*,
|
||||||
|
exit_tag: Optional[str] = None,
|
||||||
|
ordertype: Optional[str] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a trade exit for the given trade and limit
|
Executes a trade exit for the given trade and limit
|
||||||
@ -1317,14 +1307,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[exit_type]
|
order_type = ordertype or self.strategy.order_types[exit_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'] # TODO-lev update to exit
|
time_in_force = self.strategy.order_time_in_force['sell'] # TODO-lev update to exit
|
||||||
@ -1359,16 +1345,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-trading
|
# Lock pair for one candle to prevent immediate re-trading
|
||||||
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
|
||||||
|
|
||||||
@ -1399,7 +1385,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit_trade,
|
'profit_amount': profit_trade,
|
||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
'buy_tag': trade.buy_tag,
|
'buy_tag': trade.enter_tag,
|
||||||
|
'enter_tag': trade.enter_tag,
|
||||||
'sell_reason': trade.sell_reason,
|
'sell_reason': trade.sell_reason,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date or datetime.utcnow(),
|
'close_date': trade.close_date or datetime.utcnow(),
|
||||||
@ -1443,7 +1430,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit_trade,
|
'profit_amount': profit_trade,
|
||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
'buy_tag': trade.buy_tag,
|
'buy_tag': trade.enter_tag,
|
||||||
|
'enter_tag': trade.enter_tag,
|
||||||
'sell_reason': trade.sell_reason,
|
'sell_reason': trade.sell_reason,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date or datetime.now(timezone.utc),
|
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||||
@ -1465,13 +1453,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:
|
||||||
@ -1511,13 +1500,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
|
||||||
|
|
||||||
|
@ -92,7 +92,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.")
|
||||||
|
|
||||||
@ -490,11 +491,11 @@ class Backtesting:
|
|||||||
open_rate=row[OPEN_IDX],
|
open_rate=row[OPEN_IDX],
|
||||||
open_date=current_time,
|
open_date=current_time,
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
amount=round(stake_amount / row[OPEN_IDX], 8),
|
amount=round((stake_amount / row[OPEN_IDX]) * leverage, 8),
|
||||||
fee_open=self.fee,
|
fee_open=self.fee,
|
||||||
fee_close=self.fee,
|
fee_close=self.fee,
|
||||||
is_open=True,
|
is_open=True,
|
||||||
buy_tag=row[ENTER_TAG_IDX] if has_enter_tag else None,
|
enter_tag=row[ENTER_TAG_IDX] if has_enter_tag else None,
|
||||||
exchange=self._exchange_name,
|
exchange=self._exchange_name,
|
||||||
is_short=(direction == 'short'),
|
is_short=(direction == 'short'),
|
||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
|
@ -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
|
||||||
@ -422,7 +380,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
|||||||
starting_balance=start_balance,
|
starting_balance=start_balance,
|
||||||
results=results, skip_nan=False)
|
results=results, skip_nan=False)
|
||||||
|
|
||||||
buy_tag_results = generate_tag_metrics("buy_tag", starting_balance=start_balance,
|
enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance,
|
||||||
results=results, skip_nan=False)
|
results=results, skip_nan=False)
|
||||||
|
|
||||||
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
||||||
@ -448,7 +406,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
|||||||
'best_pair': best_pair,
|
'best_pair': best_pair,
|
||||||
'worst_pair': worst_pair,
|
'worst_pair': worst_pair,
|
||||||
'results_per_pair': pair_results,
|
'results_per_pair': pair_results,
|
||||||
'results_per_buy_tag': buy_tag_results,
|
'results_per_enter_tag': enter_tag_results,
|
||||||
'sell_reason_summary': sell_reason_stats,
|
'sell_reason_summary': sell_reason_stats,
|
||||||
'left_open_trades': left_open_results,
|
'left_open_trades': left_open_results,
|
||||||
# 'days_breakdown_stats': days_breakdown_stats,
|
# 'days_breakdown_stats': days_breakdown_stats,
|
||||||
@ -634,10 +592,10 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
|
|||||||
:param stake_currency: stake-currency - used to correctly name headers
|
:param stake_currency: stake-currency - used to correctly name headers
|
||||||
:return: pretty printed table with tabulate as string
|
:return: pretty printed table with tabulate as string
|
||||||
"""
|
"""
|
||||||
if(tag_type == "buy_tag"):
|
if(tag_type == "enter_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 = [
|
||||||
[
|
[
|
||||||
@ -818,10 +776,12 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
|||||||
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
if results.get('results_per_buy_tag') is not None:
|
if (results.get('results_per_enter_tag') is not None
|
||||||
|
or results.get('results_per_buy_tag') is not None):
|
||||||
|
# results_per_buy_tag is deprecated and should be removed 2 versions after short golive.
|
||||||
table = text_table_tags(
|
table = text_table_tags(
|
||||||
"buy_tag",
|
"enter_tag",
|
||||||
results['results_per_buy_tag'],
|
results.get('results_per_enter_tag', results.get('results_per_buy_tag')),
|
||||||
stake_currency=stake_currency)
|
stake_currency=stake_currency)
|
||||||
|
|
||||||
if isinstance(table, str) and len(table) > 0:
|
if isinstance(table, str) and len(table) > 0:
|
||||||
|
@ -47,7 +47,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
|||||||
min_rate = get_column_def(cols, 'min_rate', 'null')
|
min_rate = get_column_def(cols, 'min_rate', 'null')
|
||||||
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
||||||
strategy = get_column_def(cols, 'strategy', 'null')
|
strategy = get_column_def(cols, 'strategy', 'null')
|
||||||
buy_tag = get_column_def(cols, 'buy_tag', 'null')
|
enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null'))
|
||||||
|
|
||||||
trading_mode = get_column_def(cols, 'trading_mode', 'null')
|
trading_mode = get_column_def(cols, 'trading_mode', 'null')
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
|||||||
stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
|
stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
|
||||||
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
||||||
stoploss_order_id, stoploss_last_update,
|
stoploss_order_id, stoploss_last_update,
|
||||||
max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag,
|
max_rate, min_rate, sell_reason, sell_order_status, strategy, enter_tag,
|
||||||
timeframe, open_trade_value, close_profit_abs,
|
timeframe, open_trade_value, close_profit_abs,
|
||||||
trading_mode, leverage, isolated_liq, is_short,
|
trading_mode, leverage, isolated_liq, is_short,
|
||||||
interest_rate, funding_fees
|
interest_rate, funding_fees
|
||||||
@ -116,7 +116,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
|||||||
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
|
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
|
||||||
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
|
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
|
||||||
{sell_order_status} sell_order_status,
|
{sell_order_status} sell_order_status,
|
||||||
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe,
|
{strategy} strategy, {enter_tag} enter_tag, {timeframe} timeframe,
|
||||||
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
|
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
|
||||||
{trading_mode} trading_mode, {leverage} leverage, {isolated_liq} isolated_liq,
|
{trading_mode} trading_mode, {leverage} leverage, {isolated_liq} isolated_liq,
|
||||||
{is_short} is_short, {interest_rate} interest_rate,
|
{is_short} is_short, {interest_rate} interest_rate,
|
||||||
@ -180,7 +180,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
|||||||
table_back_name = get_backup_name(tabs, 'trades_bak')
|
table_back_name = get_backup_name(tabs, 'trades_bak')
|
||||||
|
|
||||||
# Check for latest column
|
# Check for latest column
|
||||||
if not has_column(cols, 'funding_fees'):
|
if not has_column(cols, 'enter_tag'):
|
||||||
logger.info(f'Running database migration for trades - backup: {table_back_name}')
|
logger.info(f'Running database migration for trades - backup: {table_back_name}')
|
||||||
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
|
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
|
||||||
# Reread columns - the above recreated the table!
|
# Reread columns - the above recreated the table!
|
||||||
|
@ -264,7 +264,7 @@ class LocalTrade():
|
|||||||
sell_reason: str = ''
|
sell_reason: str = ''
|
||||||
sell_order_status: str = ''
|
sell_order_status: str = ''
|
||||||
strategy: str = ''
|
strategy: str = ''
|
||||||
buy_tag: Optional[str] = None
|
enter_tag: Optional[str] = None
|
||||||
timeframe: Optional[int] = None
|
timeframe: Optional[int] = None
|
||||||
|
|
||||||
trading_mode: TradingMode = TradingMode.SPOT
|
trading_mode: TradingMode = TradingMode.SPOT
|
||||||
@ -280,6 +280,14 @@ class LocalTrade():
|
|||||||
# Futures properties
|
# Futures properties
|
||||||
funding_fees: Optional[float] = None
|
funding_fees: Optional[float] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def buy_tag(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Compatibility between buy_tag (old) and enter_tag (new)
|
||||||
|
Consider buy_tag deprecated
|
||||||
|
"""
|
||||||
|
return self.enter_tag
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_no_leverage(self) -> bool:
|
def has_no_leverage(self) -> bool:
|
||||||
"""Returns true if this is a non-leverage, non-short trade"""
|
"""Returns true if this is a non-leverage, non-short trade"""
|
||||||
@ -389,7 +397,8 @@ class LocalTrade():
|
|||||||
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
||||||
'stake_amount': round(self.stake_amount, 8),
|
'stake_amount': round(self.stake_amount, 8),
|
||||||
'strategy': self.strategy,
|
'strategy': self.strategy,
|
||||||
'buy_tag': self.buy_tag,
|
'buy_tag': self.enter_tag,
|
||||||
|
'enter_tag': self.enter_tag,
|
||||||
'timeframe': self.timeframe,
|
'timeframe': self.timeframe,
|
||||||
|
|
||||||
'fee_open': self.fee_open,
|
'fee_open': self.fee_open,
|
||||||
@ -928,7 +937,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
sell_reason = Column(String(100), nullable=True)
|
sell_reason = Column(String(100), nullable=True)
|
||||||
sell_order_status = Column(String(100), nullable=True)
|
sell_order_status = Column(String(100), nullable=True)
|
||||||
strategy = Column(String(100), nullable=True)
|
strategy = Column(String(100), nullable=True)
|
||||||
buy_tag = Column(String(100), nullable=True)
|
enter_tag = Column(String(100), nullable=True)
|
||||||
timeframe = Column(Integer, nullable=True)
|
timeframe = Column(Integer, nullable=True)
|
||||||
|
|
||||||
trading_mode = Column(Enum(TradingMode), nullable=True)
|
trading_mode = Column(Enum(TradingMode), nullable=True)
|
||||||
@ -1099,7 +1108,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_buy_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
def get_enter_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns List of dicts containing all Trades, based on buy tag performance
|
Returns List of dicts containing all Trades, based on buy tag performance
|
||||||
Can either be average for all pairs or a specific pair provided
|
Can either be average for all pairs or a specific pair provided
|
||||||
@ -1110,25 +1119,25 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
if(pair is not None):
|
if(pair is not None):
|
||||||
filters.append(Trade.pair == pair)
|
filters.append(Trade.pair == pair)
|
||||||
|
|
||||||
buy_tag_perf = Trade.query.with_entities(
|
enter_tag_perf = Trade.query.with_entities(
|
||||||
Trade.buy_tag,
|
Trade.enter_tag,
|
||||||
func.sum(Trade.close_profit).label('profit_sum'),
|
func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||||
func.count(Trade.pair).label('count')
|
func.count(Trade.pair).label('count')
|
||||||
).filter(*filters)\
|
).filter(*filters)\
|
||||||
.group_by(Trade.buy_tag) \
|
.group_by(Trade.enter_tag) \
|
||||||
.order_by(desc('profit_sum_abs')) \
|
.order_by(desc('profit_sum_abs')) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'buy_tag': buy_tag if buy_tag is not None else "Other",
|
'enter_tag': enter_tag if enter_tag is not None else "Other",
|
||||||
'profit_ratio': profit,
|
'profit_ratio': profit,
|
||||||
'profit_pct': round(profit * 100, 2),
|
'profit_pct': round(profit * 100, 2),
|
||||||
'profit_abs': profit_abs,
|
'profit_abs': profit_abs,
|
||||||
'count': count
|
'count': count
|
||||||
}
|
}
|
||||||
for buy_tag, profit, profit_abs, count in buy_tag_perf
|
for enter_tag, profit, profit_abs, count in enter_tag_perf
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -1178,7 +1187,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
|
|
||||||
mix_tag_perf = Trade.query.with_entities(
|
mix_tag_perf = Trade.query.with_entities(
|
||||||
Trade.id,
|
Trade.id,
|
||||||
Trade.buy_tag,
|
Trade.enter_tag,
|
||||||
Trade.sell_reason,
|
Trade.sell_reason,
|
||||||
func.sum(Trade.close_profit).label('profit_sum'),
|
func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||||
@ -1189,12 +1198,12 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
.all()
|
.all()
|
||||||
|
|
||||||
return_list: List[Dict] = []
|
return_list: List[Dict] = []
|
||||||
for id, buy_tag, sell_reason, profit, profit_abs, count in mix_tag_perf:
|
for id, enter_tag, sell_reason, profit, profit_abs, count in mix_tag_perf:
|
||||||
buy_tag = buy_tag if buy_tag is not None else "Other"
|
enter_tag = enter_tag if enter_tag is not None else "Other"
|
||||||
sell_reason = sell_reason if sell_reason is not None else "Other"
|
sell_reason = sell_reason if sell_reason is not None else "Other"
|
||||||
|
|
||||||
if(sell_reason is not None and buy_tag is not None):
|
if(sell_reason is not None and enter_tag is not None):
|
||||||
mix_tag = buy_tag + " " + sell_reason
|
mix_tag = enter_tag + " " + sell_reason
|
||||||
i = 0
|
i = 0
|
||||||
if not any(item["mix_tag"] == mix_tag for item in return_list):
|
if not any(item["mix_tag"] == mix_tag for item in return_list):
|
||||||
return_list.append({'mix_tag': mix_tag,
|
return_list.append({'mix_tag': mix_tag,
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
# 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')
|
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
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@ -125,19 +126,24 @@ 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 OrderTypeValues(str, Enum):
|
||||||
|
limit = 'limit'
|
||||||
|
market = 'market'
|
||||||
|
|
||||||
|
|
||||||
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]
|
||||||
|
|
||||||
@ -185,7 +191,8 @@ class TradeSchema(BaseModel):
|
|||||||
amount_requested: float
|
amount_requested: float
|
||||||
stake_amount: float
|
stake_amount: float
|
||||||
strategy: str
|
strategy: str
|
||||||
buy_tag: Optional[str]
|
buy_tag: Optional[str] # Deprecated
|
||||||
|
enter_tag: Optional[str]
|
||||||
timeframe: int
|
timeframe: int
|
||||||
fee_open: Optional[float]
|
fee_open: Optional[float]
|
||||||
fee_open_cost: Optional[float]
|
fee_open_cost: Optional[float]
|
||||||
@ -277,10 +284,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):
|
||||||
|
@ -29,7 +29,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# API version
|
# API version
|
||||||
# Pre-1.1, no version was provided
|
# Pre-1.1, no version was provided
|
||||||
# Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen.
|
# Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen.
|
||||||
API_VERSION = 1.1
|
# 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()
|
||||||
@ -129,7 +130,8 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
|
|||||||
|
|
||||||
@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())
|
||||||
@ -139,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'])
|
||||||
|
@ -646,7 +646,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
|
||||||
@ -671,7 +671,11 @@ class RPC:
|
|||||||
current_rate = self._freqtrade.exchange.get_rate(
|
current_rate = self._freqtrade.exchange.get_rate(
|
||||||
trade.pair, refresh=False, side=closing_side)
|
trade.pair, refresh=False, side=closing_side)
|
||||||
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:
|
||||||
@ -699,7 +703,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
|
||||||
@ -727,7 +732,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
|
||||||
@ -782,27 +790,23 @@ class RPC:
|
|||||||
|
|
||||||
return pair_rates
|
return pair_rates
|
||||||
|
|
||||||
def _rpc_buy_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
def _rpc_enter_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Handler for buy tag performance.
|
Handler for buy tag performance.
|
||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
"""
|
"""
|
||||||
buy_tags = Trade.get_buy_tag_performance(pair)
|
return Trade.get_enter_tag_performance(pair)
|
||||||
|
|
||||||
return buy_tags
|
|
||||||
|
|
||||||
def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Handler for sell reason performance.
|
Handler for sell reason performance.
|
||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
"""
|
"""
|
||||||
sell_reasons = Trade.get_sell_reason_performance(pair)
|
return Trade.get_sell_reason_performance(pair)
|
||||||
|
|
||||||
return sell_reasons
|
|
||||||
|
|
||||||
def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Handler for mix tag (buy_tag + sell_reason) performance.
|
Handler for mix tag (enter_tag + sell_reason) performance.
|
||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
"""
|
"""
|
||||||
mix_tags = Trade.get_mix_tag_performance(pair)
|
mix_tags = Trade.get_mix_tag_performance(pair)
|
||||||
|
@ -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]
|
||||||
@ -154,7 +155,7 @@ class Telegram(RPCHandler):
|
|||||||
CommandHandler('trades', self._trades),
|
CommandHandler('trades', self._trades),
|
||||||
CommandHandler('delete', self._delete_trade),
|
CommandHandler('delete', self._delete_trade),
|
||||||
CommandHandler('performance', self._performance),
|
CommandHandler('performance', self._performance),
|
||||||
CommandHandler('buys', self._buy_tag_performance),
|
CommandHandler(['buys', 'entries'], self._enter_tag_performance),
|
||||||
CommandHandler('sells', self._sell_reason_performance),
|
CommandHandler('sells', self._sell_reason_performance),
|
||||||
CommandHandler('mix_tags', self._mix_tag_performance),
|
CommandHandler('mix_tags', self._mix_tag_performance),
|
||||||
CommandHandler('stats', self._stats),
|
CommandHandler('stats', self._stats),
|
||||||
@ -182,7 +183,8 @@ class Telegram(RPCHandler):
|
|||||||
CallbackQueryHandler(self._profit, pattern='update_profit'),
|
CallbackQueryHandler(self._profit, pattern='update_profit'),
|
||||||
CallbackQueryHandler(self._balance, pattern='update_balance'),
|
CallbackQueryHandler(self._balance, pattern='update_balance'),
|
||||||
CallbackQueryHandler(self._performance, pattern='update_performance'),
|
CallbackQueryHandler(self._performance, pattern='update_performance'),
|
||||||
CallbackQueryHandler(self._buy_tag_performance, pattern='update_buy_tag_performance'),
|
CallbackQueryHandler(self._enter_tag_performance,
|
||||||
|
pattern='update_enter_tag_performance'),
|
||||||
CallbackQueryHandler(self._sell_reason_performance,
|
CallbackQueryHandler(self._sell_reason_performance,
|
||||||
pattern='update_sell_reason_performance'),
|
pattern='update_sell_reason_performance'),
|
||||||
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
||||||
@ -226,7 +228,7 @@ class Telegram(RPCHandler):
|
|||||||
f"{emoji} *{msg['exchange']}:* {'Bought' if is_fill else 'Buying'} {msg['pair']}"
|
f"{emoji} *{msg['exchange']}:* {'Bought' if is_fill else 'Buying'} {msg['pair']}"
|
||||||
f" (#{msg['trade_id']})\n"
|
f" (#{msg['trade_id']})\n"
|
||||||
)
|
)
|
||||||
message += f"*Buy Tag:* `{msg['buy_tag']}`\n" if msg.get('buy_tag', None) else ""
|
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else ""
|
||||||
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||||
|
|
||||||
if msg['type'] == RPCMessageType.BUY_FILL:
|
if msg['type'] == RPCMessageType.BUY_FILL:
|
||||||
@ -251,7 +253,7 @@ class Telegram(RPCHandler):
|
|||||||
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
||||||
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
||||||
|
|
||||||
msg['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None
|
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
|
||||||
msg['emoji'] = self._get_sell_emoji(msg)
|
msg['emoji'] = self._get_sell_emoji(msg)
|
||||||
|
|
||||||
# Check if all sell properties are available.
|
# Check if all sell properties are available.
|
||||||
@ -271,7 +273,7 @@ class Telegram(RPCHandler):
|
|||||||
f"{'Sold' if is_fill else 'Selling'} {msg['pair']} (#{msg['trade_id']})\n"
|
f"{'Sold' if is_fill else 'Selling'} {msg['pair']} (#{msg['trade_id']})\n"
|
||||||
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
|
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
|
||||||
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
||||||
f"*Buy Tag:* `{msg['buy_tag']}`\n"
|
f"*Enter Tag:* `{msg['enter_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"
|
||||||
@ -397,7 +399,7 @@ class Telegram(RPCHandler):
|
|||||||
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`",
|
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`",
|
||||||
"*Current Pair:* {pair}",
|
"*Current Pair:* {pair}",
|
||||||
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
||||||
"*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "",
|
"*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
|
||||||
"*Open Rate:* `{open_rate:.8f}`",
|
"*Open Rate:* `{open_rate:.8f}`",
|
||||||
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
||||||
"*Current Rate:* `{current_rate:.8f}`",
|
"*Current Rate:* `{current_rate:.8f}`",
|
||||||
@ -972,7 +974,7 @@ class Telegram(RPCHandler):
|
|||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _buy_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /buys PAIR .
|
Handler for /buys PAIR .
|
||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
@ -985,11 +987,11 @@ class Telegram(RPCHandler):
|
|||||||
if context.args and isinstance(context.args[0], str):
|
if context.args and isinstance(context.args[0], str):
|
||||||
pair = context.args[0]
|
pair = context.args[0]
|
||||||
|
|
||||||
trades = self._rpc._rpc_buy_tag_performance(pair)
|
trades = self._rpc._rpc_enter_tag_performance(pair)
|
||||||
output = "<b>Buy Tag Performance:</b>\n"
|
output = "<b>Buy Tag Performance:</b>\n"
|
||||||
for i, trade in enumerate(trades):
|
for i, trade in enumerate(trades):
|
||||||
stat_line = (
|
stat_line = (
|
||||||
f"{i+1}.\t <code>{trade['buy_tag']}\t"
|
f"{i+1}.\t <code>{trade['enter_tag']}\t"
|
||||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
f"({trade['profit_ratio']:.2%}) "
|
f"({trade['profit_ratio']:.2%}) "
|
||||||
f"({trade['count']})</code>\n")
|
f"({trade['count']})</code>\n")
|
||||||
@ -1001,7 +1003,7 @@ class Telegram(RPCHandler):
|
|||||||
output += stat_line
|
output += stat_line
|
||||||
|
|
||||||
self._send_msg(output, parse_mode=ParseMode.HTML,
|
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||||
reload_able=True, callback_path="update_buy_tag_performance",
|
reload_able=True, callback_path="update_enter_tag_performance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
@ -1277,7 +1279,8 @@ class Telegram(RPCHandler):
|
|||||||
" *table :* `will display trades in a table`\n"
|
" *table :* `will display trades in a table`\n"
|
||||||
" `pending buy orders are marked with an asterisk (*)`\n"
|
" `pending buy orders are marked with an asterisk (*)`\n"
|
||||||
" `pending sell orders are marked with a double asterisk (**)`\n"
|
" `pending sell orders are marked with a double asterisk (**)`\n"
|
||||||
"*/buys <pair|none>:* `Shows the buy_tag performance`\n"
|
# TODO-lev: Update commands and help (?)
|
||||||
|
"*/buys <pair|none>:* `Shows the enter_tag performance`\n"
|
||||||
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
|
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
|
||||||
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
|
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
|
||||||
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
||||||
|
@ -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"""
|
||||||
|
|
||||||
|
success = False
|
||||||
|
attempts = 0
|
||||||
|
while not success and attempts <= self._retries:
|
||||||
|
if attempts:
|
||||||
|
if self._retry_delay:
|
||||||
|
time.sleep(self._retry_delay)
|
||||||
|
logger.info("Retrying webhook...")
|
||||||
|
|
||||||
|
attempts += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self._format == 'form':
|
if self._format == 'form':
|
||||||
post(self._url, data=payload)
|
response = post(self._url, data=payload)
|
||||||
elif self._format == 'json':
|
elif self._format == 'json':
|
||||||
post(self._url, json=payload)
|
response = post(self._url, json=payload)
|
||||||
|
elif self._format == 'raw':
|
||||||
|
response = post(self._url, data=payload['data'],
|
||||||
|
headers={'Content-Type': 'text/plain'})
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError('Unknown format: {}'.format(self._format))
|
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:
|
except RequestException as exc:
|
||||||
logger.warning("Could not call webhook url. Exception: %s", exc)
|
logger.warning("Could not call webhook url. Exception: %s", exc)
|
||||||
|
@ -87,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,5 +1,7 @@
|
|||||||
|
|
||||||
plot_config = {
|
@property
|
||||||
|
def plot_config(self):
|
||||||
|
return {
|
||||||
# Main plot indicators (Moving averages, ...)
|
# Main plot indicators (Moving averages, ...)
|
||||||
'main_plot': {
|
'main_plot': {
|
||||||
'tema': {},
|
'tema': {},
|
||||||
@ -15,4 +17,4 @@ plot_config = {
|
|||||||
'rsi': {'color': 'red'},
|
'rsi': {'color': 'red'},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,8 +82,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.5
|
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,7 +2,7 @@
|
|||||||
-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.4.0
|
filelock==3.4.0
|
||||||
|
@ -2,7 +2,7 @@ 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.92
|
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==36.0.0
|
cryptography==36.0.0
|
||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
@ -34,14 +34,14 @@ 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
|
||||||
|
|
||||||
|
@ -215,8 +215,6 @@ def patch_get_signal(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
:param mocker: mocker to patch IStrategy class
|
:param mocker: mocker to patch IStrategy class
|
||||||
:param value: which value IStrategy.get_signal() must return
|
|
||||||
(buy, sell, buy_tag)
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# returns (Signal-direction, signaname)
|
# returns (Signal-direction, signaname)
|
||||||
|
@ -102,7 +102,7 @@ def mock_trade_2(fee, is_short: bool):
|
|||||||
open_order_id=f'dry_run_sell_{direc(is_short)}_12345',
|
open_order_id=f'dry_run_sell_{direc(is_short)}_12345',
|
||||||
strategy='StrategyTestV3',
|
strategy='StrategyTestV3',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
buy_tag='TEST1',
|
enter_tag='TEST1',
|
||||||
sell_reason='sell_signal',
|
sell_reason='sell_signal',
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||||
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
|
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
|
||||||
@ -258,7 +258,7 @@ def mock_trade_5(fee, is_short: bool):
|
|||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
buy_tag='TEST1',
|
enter_tag='TEST1',
|
||||||
stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455',
|
stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
is_short=is_short
|
is_short=is_short
|
||||||
@ -314,7 +314,7 @@ def mock_trade_6(fee, is_short: bool):
|
|||||||
open_rate=0.15,
|
open_rate=0.15,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
buy_tag='TEST2',
|
enter_tag='TEST2',
|
||||||
open_order_id=f"prod_sell_{direc(is_short)}_6",
|
open_order_id=f"prod_sell_{direc(is_short)}_6",
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
is_short=is_short
|
is_short=is_short
|
||||||
|
@ -1747,6 +1747,7 @@ 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]}, candleType ...",
|
f"timeframe {pairs[0][1]}, candleType ...",
|
||||||
caplog)
|
caplog)
|
||||||
@ -1755,6 +1756,14 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
|||||||
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
|
||||||
@ -1852,7 +1861,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():
|
||||||
|
@ -36,6 +36,7 @@ class BTContainer(NamedTuple):
|
|||||||
trailing_stop_positive_offset: float = 0.0
|
trailing_stop_positive_offset: float = 0.0
|
||||||
use_sell_signal: bool = False
|
use_sell_signal: bool = False
|
||||||
use_custom_stoploss: bool = False
|
use_custom_stoploss: bool = False
|
||||||
|
leverage: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
def _get_frame_time_from_offset(offset):
|
def _get_frame_time_from_offset(offset):
|
||||||
|
@ -536,6 +536,23 @@ tc33 = BTContainer(data=[
|
|||||||
)]
|
)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Test 34: (copy of test25 with leverage)
|
||||||
|
# Sell with signal sell in candle 3 (stoploss also triggers on this candle)
|
||||||
|
# Stoploss at 1%.
|
||||||
|
# Sell-signal wins over stoploss
|
||||||
|
tc34 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||||
|
[2, 4987, 5012, 4986, 4986, 6172, 0, 0],
|
||||||
|
[3, 5010, 5010, 4986, 5010, 6172, 0, 1],
|
||||||
|
[4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on
|
||||||
|
[5, 4995, 4995, 4950, 4950, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True,
|
||||||
|
leverage=5.0,
|
||||||
|
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)]
|
||||||
|
)
|
||||||
|
|
||||||
TESTS = [
|
TESTS = [
|
||||||
tc0,
|
tc0,
|
||||||
tc1,
|
tc1,
|
||||||
@ -571,6 +588,7 @@ TESTS = [
|
|||||||
tc31,
|
tc31,
|
||||||
tc32,
|
tc32,
|
||||||
tc33,
|
tc33,
|
||||||
|
tc34,
|
||||||
# TODO-lev: Add tests for short here
|
# TODO-lev: Add tests for short here
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -593,14 +611,19 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
|||||||
|
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0)
|
mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
|
mocker.patch("freqtrade.exchange.Binance.get_max_leverage", return_value=100)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
frame = _build_backtest_dataframe(data.data)
|
frame = _build_backtest_dataframe(data.data)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
backtesting.required_startup = 0
|
backtesting.required_startup = 0
|
||||||
|
if data.leverage > 1.0:
|
||||||
|
# TODO-lev: Should we initialize this properly??
|
||||||
|
backtesting._can_short = True
|
||||||
backtesting.strategy.advise_entry = lambda a, m: frame
|
backtesting.strategy.advise_entry = lambda a, m: frame
|
||||||
backtesting.strategy.advise_exit = lambda a, m: frame
|
backtesting.strategy.advise_exit = lambda a, m: frame
|
||||||
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
|
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
|
||||||
|
backtesting.strategy.leverage = lambda **kwargs: data.leverage
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
pair = "UNITTEST/BTC"
|
pair = "UNITTEST/BTC"
|
||||||
@ -621,6 +644,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
|||||||
for c, trade in enumerate(data.trades):
|
for c, trade in enumerate(data.trades):
|
||||||
res = results.iloc[c]
|
res = results.iloc[c]
|
||||||
assert res.sell_reason == trade.sell_reason.value
|
assert res.sell_reason == trade.sell_reason.value
|
||||||
assert res.buy_tag == trade.enter_tag
|
assert res.enter_tag == trade.enter_tag
|
||||||
assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
|
assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
|
||||||
assert res.close_date == _get_frame_time_from_offset(trade.close_tick)
|
assert res.close_date == _get_frame_time_from_offset(trade.close_tick)
|
||||||
|
@ -441,7 +441,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({
|
||||||
@ -473,7 +474,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"}]
|
||||||
@ -698,7 +700,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
|||||||
'min_rate': [0.10370188, 0.10300000000000001],
|
'min_rate': [0.10370188, 0.10300000000000001],
|
||||||
'max_rate': [0.10501, 0.1038888],
|
'max_rate': [0.10501, 0.1038888],
|
||||||
'is_open': [False, False],
|
'is_open': [False, False],
|
||||||
'buy_tag': [None, None],
|
'enter_tag': [None, None],
|
||||||
"is_short": [False, False],
|
"is_short": [False, False],
|
||||||
})
|
})
|
||||||
pd.testing.assert_frame_equal(results, expected)
|
pd.testing.assert_frame_equal(results, expected)
|
||||||
|
@ -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')
|
||||||
|
@ -70,6 +70,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'max_rate': ANY,
|
'max_rate': ANY,
|
||||||
'strategy': ANY,
|
'strategy': ANY,
|
||||||
'buy_tag': ANY,
|
'buy_tag': ANY,
|
||||||
|
'enter_tag': ANY,
|
||||||
'timeframe': 5,
|
'timeframe': 5,
|
||||||
'open_order_id': ANY,
|
'open_order_id': ANY,
|
||||||
'close_date': None,
|
'close_date': None,
|
||||||
@ -143,6 +144,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'max_rate': ANY,
|
'max_rate': ANY,
|
||||||
'strategy': ANY,
|
'strategy': ANY,
|
||||||
'buy_tag': ANY,
|
'buy_tag': ANY,
|
||||||
|
'enter_tag': ANY,
|
||||||
'timeframe': ANY,
|
'timeframe': ANY,
|
||||||
'open_order_id': ANY,
|
'open_order_id': ANY,
|
||||||
'close_date': None,
|
'close_date': None,
|
||||||
@ -842,7 +844,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
|||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
||||||
|
|
||||||
|
|
||||||
def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
def test_enter_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||||
limit_sell_order, mocker) -> None:
|
limit_sell_order, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -869,23 +871,23 @@ def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
|||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
res = rpc._rpc_buy_tag_performance(None)
|
res = rpc._rpc_enter_tag_performance(None)
|
||||||
|
|
||||||
assert len(res) == 1
|
assert len(res) == 1
|
||||||
assert res[0]['buy_tag'] == 'Other'
|
assert res[0]['enter_tag'] == 'Other'
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
||||||
|
|
||||||
trade.buy_tag = "TEST_TAG"
|
trade.enter_tag = "TEST_TAG"
|
||||||
res = rpc._rpc_buy_tag_performance(None)
|
res = rpc._rpc_enter_tag_performance(None)
|
||||||
|
|
||||||
assert len(res) == 1
|
assert len(res) == 1
|
||||||
assert res[0]['buy_tag'] == 'TEST_TAG'
|
assert res[0]['enter_tag'] == 'TEST_TAG'
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
||||||
|
|
||||||
|
|
||||||
def test_buy_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -896,21 +898,21 @@ def test_buy_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
|||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
res = rpc._rpc_buy_tag_performance(None)
|
res = rpc._rpc_enter_tag_performance(None)
|
||||||
|
|
||||||
assert len(res) == 2
|
assert len(res) == 2
|
||||||
assert res[0]['buy_tag'] == 'TEST1'
|
assert res[0]['enter_tag'] == 'TEST1'
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||||
assert res[1]['buy_tag'] == 'Other'
|
assert res[1]['enter_tag'] == 'Other'
|
||||||
assert res[1]['count'] == 1
|
assert res[1]['count'] == 1
|
||||||
assert prec_satoshi(res[1]['profit_pct'], 1.0)
|
assert prec_satoshi(res[1]['profit_pct'], 1.0)
|
||||||
|
|
||||||
# Test for a specific pair
|
# Test for a specific pair
|
||||||
res = rpc._rpc_buy_tag_performance('ETC/BTC')
|
res = rpc._rpc_enter_tag_performance('ETC/BTC')
|
||||||
assert len(res) == 1
|
assert len(res) == 1
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert res[0]['buy_tag'] == 'TEST1'
|
assert res[0]['enter_tag'] == 'TEST1'
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||||
|
|
||||||
|
|
||||||
@ -1020,7 +1022,7 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
|||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
||||||
|
|
||||||
trade.buy_tag = "TESTBUY"
|
trade.enter_tag = "TESTBUY"
|
||||||
trade.sell_reason = "TESTSELL"
|
trade.sell_reason = "TESTSELL"
|
||||||
res = rpc._rpc_mix_tag_performance(None)
|
res = rpc._rpc_mix_tag_performance(None)
|
||||||
|
|
||||||
@ -1107,7 +1109,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
|
||||||
|
@ -958,6 +958,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
|||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'strategy': CURRENT_TEST_STRATEGY,
|
'strategy': CURRENT_TEST_STRATEGY,
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
|
'enter_tag': None,
|
||||||
'timeframe': 5,
|
'timeframe': 5,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
}
|
}
|
||||||
@ -1116,6 +1117,7 @@ def test_api_forcebuy(botclient, mocker, fee):
|
|||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'strategy': CURRENT_TEST_STRATEGY,
|
'strategy': CURRENT_TEST_STRATEGY,
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
|
'enter_tag': None,
|
||||||
'timeframe': 5,
|
'timeframe': 5,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
}
|
}
|
||||||
|
@ -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 (CURRENT_TEST_STRATEGY, create_mock_trades, get_patched_freqtradebot,
|
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_patched_freqtradebot,
|
||||||
log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist)
|
log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist)
|
||||||
@ -93,7 +94,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
|
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
|
||||||
"['delete'], ['performance'], ['buys'], ['sells'], ['mix_tags'], "
|
"['delete'], ['performance'], ['buys', 'entries'], ['sells'], ['mix_tags'], "
|
||||||
"['stats'], ['daily'], ['weekly'], ['monthly'], "
|
"['stats'], ['daily'], ['weekly'], ['monthly'], "
|
||||||
"['count'], ['locks'], ['unlock', 'delete_locks'], "
|
"['count'], ['locks'], ['unlock', 'delete_locks'], "
|
||||||
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], "
|
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], "
|
||||||
@ -189,6 +190,7 @@ def test_telegram_status(default_conf, update, mocker) -> None:
|
|||||||
'amount': 90.99181074,
|
'amount': 90.99181074,
|
||||||
'stake_amount': 90.99181074,
|
'stake_amount': 90.99181074,
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
|
'enter_tag': None,
|
||||||
'close_profit_ratio': None,
|
'close_profit_ratio': None,
|
||||||
'profit': -0.0059,
|
'profit': -0.0059,
|
||||||
'profit_ratio': -0.0059,
|
'profit_ratio': -0.0059,
|
||||||
@ -937,7 +939,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,
|
||||||
@ -954,6 +956,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'buy_tag': ANY,
|
'buy_tag': ANY,
|
||||||
|
'enter_tag': ANY,
|
||||||
'sell_reason': SellType.FORCE_SELL.value,
|
'sell_reason': SellType.FORCE_SELL.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_date': ANY,
|
'close_date': ANY,
|
||||||
@ -1001,7 +1004,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,
|
||||||
@ -1018,6 +1021,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
|
|||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'buy_tag': ANY,
|
'buy_tag': ANY,
|
||||||
|
'enter_tag': ANY,
|
||||||
'sell_reason': SellType.FORCE_SELL.value,
|
'sell_reason': SellType.FORCE_SELL.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_date': ANY,
|
'close_date': ANY,
|
||||||
@ -1055,7 +1059,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,
|
||||||
@ -1072,6 +1076,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
|||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'buy_tag': ANY,
|
'buy_tag': ANY,
|
||||||
|
'enter_tag': ANY,
|
||||||
'sell_reason': SellType.FORCE_SELL.value,
|
'sell_reason': SellType.FORCE_SELL.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_date': ANY,
|
'close_date': ANY,
|
||||||
@ -1187,7 +1192,7 @@ 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(
|
||||||
@ -1217,7 +1222,7 @@ 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',
|
||||||
@ -1235,20 +1240,32 @@ def test_buy_tag_performance_handle(default_conf, update, ticker, fee,
|
|||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
trade.buy_tag = "TESTBUY"
|
trade.enter_tag = "TESTBUY"
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
# Simulate fulfilled LIMIT_SELL order for trade
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
|
|
||||||
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._enter_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._enter_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()
|
||||||
|
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_enter_tag_performance',
|
||||||
|
side_effect=RPCException('Error'))
|
||||||
|
telegram._enter_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:
|
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -1272,14 +1289,26 @@ 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',
|
||||||
@ -1297,7 +1326,7 @@ def test_mix_tag_performance_handle(default_conf, update, ticker, fee,
|
|||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
trade.buy_tag = "TESTBUY"
|
trade.enter_tag = "TESTBUY"
|
||||||
trade.sell_reason = "TESTSELL"
|
trade.sell_reason = "TESTSELL"
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
# Simulate fulfilled LIMIT_SELL order for trade
|
||||||
@ -1306,12 +1335,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(
|
||||||
@ -1598,7 +1640,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
|
|||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.BUY,
|
'type': RPCMessageType.BUY,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'buy_tag': 'buy_signal_01',
|
'enter_tag': 'buy_signal_01',
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'limit': 1.099e-05,
|
'limit': 1.099e-05,
|
||||||
@ -1616,7 +1658,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
|
|||||||
telegram.send_msg(msg)
|
telegram.send_msg(msg)
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== '\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' \
|
== '\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' \
|
||||||
'*Buy Tag:* `buy_signal_01`\n' \
|
'*Enter Tag:* `buy_signal_01`\n' \
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Amount:* `1333.33333333`\n' \
|
||||||
'*Open Rate:* `0.00001099`\n' \
|
'*Open Rate:* `0.00001099`\n' \
|
||||||
'*Current Rate:* `0.00001099`\n' \
|
'*Current Rate:* `0.00001099`\n' \
|
||||||
@ -1644,7 +1686,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.BUY_CANCEL,
|
'type': RPCMessageType.BUY_CANCEL,
|
||||||
'buy_tag': 'buy_signal_01',
|
'enter_tag': 'buy_signal_01',
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
@ -1691,7 +1733,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
|
|||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.BUY_FILL,
|
'type': RPCMessageType.BUY_FILL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'buy_tag': 'buy_signal_01',
|
'enter_tag': 'buy_signal_01',
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
@ -1705,7 +1747,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== '\N{CHECK MARK} *Binance:* Bought ETH/BTC (#1)\n' \
|
== '\N{CHECK MARK} *Binance:* Bought ETH/BTC (#1)\n' \
|
||||||
'*Buy Tag:* `buy_signal_01`\n' \
|
'*Enter Tag:* `buy_signal_01`\n' \
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Amount:* `1333.33333333`\n' \
|
||||||
'*Open Rate:* `0.00001099`\n' \
|
'*Open Rate:* `0.00001099`\n' \
|
||||||
'*Total:* `(0.00100000 BTC, 12.345 USD)`'
|
'*Total:* `(0.00100000 BTC, 12.345 USD)`'
|
||||||
@ -1732,7 +1774,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
|||||||
'profit_ratio': -0.57405275,
|
'profit_ratio': -0.57405275,
|
||||||
'stake_currency': 'ETH',
|
'stake_currency': 'ETH',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'buy_tag': 'buy_signal1',
|
'enter_tag': 'buy_signal1',
|
||||||
'sell_reason': SellType.STOP_LOSS.value,
|
'sell_reason': SellType.STOP_LOSS.value,
|
||||||
'open_date': arrow.utcnow().shift(hours=-1),
|
'open_date': arrow.utcnow().shift(hours=-1),
|
||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
@ -1740,7 +1782,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
|||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
|
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
|
||||||
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
||||||
'*Buy Tag:* `buy_signal1`\n'
|
'*Enter Tag:* `buy_signal1`\n'
|
||||||
'*Sell Reason:* `stop_loss`\n'
|
'*Sell Reason:* `stop_loss`\n'
|
||||||
'*Duration:* `1:00:00 (60.0 min)`\n'
|
'*Duration:* `1:00:00 (60.0 min)`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
@ -1764,7 +1806,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
|||||||
'profit_amount': -0.05746268,
|
'profit_amount': -0.05746268,
|
||||||
'profit_ratio': -0.57405275,
|
'profit_ratio': -0.57405275,
|
||||||
'stake_currency': 'ETH',
|
'stake_currency': 'ETH',
|
||||||
'buy_tag': 'buy_signal1',
|
'enter_tag': 'buy_signal1',
|
||||||
'sell_reason': SellType.STOP_LOSS.value,
|
'sell_reason': SellType.STOP_LOSS.value,
|
||||||
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
|
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
|
||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
@ -1772,7 +1814,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
|||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
|
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
|
||||||
'*Unrealized Profit:* `-57.41%`\n'
|
'*Unrealized Profit:* `-57.41%`\n'
|
||||||
'*Buy Tag:* `buy_signal1`\n'
|
'*Enter Tag:* `buy_signal1`\n'
|
||||||
'*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'
|
||||||
@ -1835,7 +1877,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None:
|
|||||||
'profit_amount': -0.05746268,
|
'profit_amount': -0.05746268,
|
||||||
'profit_ratio': -0.57405275,
|
'profit_ratio': -0.57405275,
|
||||||
'stake_currency': 'ETH',
|
'stake_currency': 'ETH',
|
||||||
'buy_tag': 'buy_signal1',
|
'enter_tag': 'buy_signal1',
|
||||||
'sell_reason': SellType.STOP_LOSS.value,
|
'sell_reason': SellType.STOP_LOSS.value,
|
||||||
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
|
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
|
||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
@ -1843,7 +1885,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None:
|
|||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('\N{WARNING SIGN} *Binance:* Sold KEY/ETH (#1)\n'
|
== ('\N{WARNING SIGN} *Binance:* Sold KEY/ETH (#1)\n'
|
||||||
'*Profit:* `-57.41%`\n'
|
'*Profit:* `-57.41%`\n'
|
||||||
'*Buy Tag:* `buy_signal1`\n'
|
'*Enter Tag:* `buy_signal1`\n'
|
||||||
'*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'
|
||||||
@ -1894,7 +1936,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.BUY,
|
'type': RPCMessageType.BUY,
|
||||||
'buy_tag': 'buy_signal_01',
|
'enter_tag': 'buy_signal_01',
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
@ -1909,7 +1951,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
|||||||
'open_date': arrow.utcnow().shift(hours=-1)
|
'open_date': arrow.utcnow().shift(hours=-1)
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n'
|
assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n'
|
||||||
'*Buy Tag:* `buy_signal_01`\n'
|
'*Enter Tag:* `buy_signal_01`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Open Rate:* `0.00001099`\n'
|
'*Open Rate:* `0.00001099`\n'
|
||||||
'*Current Rate:* `0.00001099`\n'
|
'*Current Rate:* `0.00001099`\n'
|
||||||
@ -1935,14 +1977,14 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
|||||||
'profit_ratio': -0.57405275,
|
'profit_ratio': -0.57405275,
|
||||||
'stake_currency': 'ETH',
|
'stake_currency': 'ETH',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'buy_tag': 'buy_signal1',
|
'enter_tag': 'buy_signal1',
|
||||||
'sell_reason': SellType.STOP_LOSS.value,
|
'sell_reason': SellType.STOP_LOSS.value,
|
||||||
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
|
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
|
||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
|
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
|
||||||
'*Unrealized Profit:* `-57.41%`\n'
|
'*Unrealized Profit:* `-57.41%`\n'
|
||||||
'*Buy Tag:* `buy_signal1`\n'
|
'*Enter Tag:* `buy_signal1`\n'
|
||||||
'*Sell Reason:* `stop_loss`\n'
|
'*Sell Reason:* `stop_loss`\n'
|
||||||
'*Duration:* `2:35:03 (155.1 min)`\n'
|
'*Duration:* `2:35:03 (155.1 min)`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
|
@ -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'}}
|
||||||
|
@ -2869,6 +2869,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
|
|||||||
'amount': amt,
|
'amount': amt,
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
|
'enter_tag': None,
|
||||||
'open_rate': open_rate,
|
'open_rate': open_rate,
|
||||||
'current_rate': 2.01 if is_short else 2.3,
|
'current_rate': 2.01 if is_short else 2.3,
|
||||||
'profit_amount': 0.29554455 if is_short else 5.685,
|
'profit_amount': 0.29554455 if is_short else 5.685,
|
||||||
@ -2925,6 +2926,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
|
|||||||
'amount': 29.70297029 if is_short else 30.0,
|
'amount': 29.70297029 if is_short else 30.0,
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
|
'enter_tag': None,
|
||||||
'open_rate': 2.02 if is_short else 2.0,
|
'open_rate': 2.02 if is_short else 2.0,
|
||||||
'current_rate': 2.2 if is_short else 2.0,
|
'current_rate': 2.2 if is_short else 2.0,
|
||||||
'profit_amount': -5.65990099 if is_short else -0.00075,
|
'profit_amount': -5.65990099 if is_short else -0.00075,
|
||||||
@ -3002,6 +3004,7 @@ def test_execute_trade_exit_custom_exit_price(
|
|||||||
'amount': amount,
|
'amount': amount,
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
|
'enter_tag': None,
|
||||||
'open_rate': open_rate,
|
'open_rate': open_rate,
|
||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit_amount,
|
'profit_amount': profit_amount,
|
||||||
@ -3066,6 +3069,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
|
|||||||
'amount': 29.70297029 if is_short else 30.0,
|
'amount': 29.70297029 if is_short else 30.0,
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
|
'enter_tag': None,
|
||||||
'open_rate': 2.02 if is_short else 2.0,
|
'open_rate': 2.02 if is_short else 2.0,
|
||||||
'current_rate': 2.2 if is_short else 2.0,
|
'current_rate': 2.2 if is_short else 2.0,
|
||||||
'profit_amount': -0.3 if is_short else -0.8985,
|
'profit_amount': -0.3 if is_short else -0.8985,
|
||||||
@ -3308,7 +3312,7 @@ def test_execute_trade_exit_market_order(
|
|||||||
assert trade.close_profit == profit_ratio
|
assert trade.close_profit == profit_ratio
|
||||||
|
|
||||||
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,
|
||||||
@ -3319,6 +3323,7 @@ def test_execute_trade_exit_market_order(
|
|||||||
'amount': round(amount, 9),
|
'amount': round(amount, 9),
|
||||||
'order_type': 'market',
|
'order_type': 'market',
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
|
'enter_tag': None,
|
||||||
'open_rate': open_rate,
|
'open_rate': open_rate,
|
||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit_amount,
|
'profit_amount': profit_amount,
|
||||||
|
@ -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'),
|
||||||
|
@ -1551,7 +1551,7 @@ def test_to_json(default_conf, fee):
|
|||||||
open_date=arrow.utcnow().shift(hours=-2).datetime,
|
open_date=arrow.utcnow().shift(hours=-2).datetime,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
buy_tag=None,
|
enter_tag=None,
|
||||||
open_order_id='dry_run_buy_12345'
|
open_order_id='dry_run_buy_12345'
|
||||||
)
|
)
|
||||||
result = trade.to_json()
|
result = trade.to_json()
|
||||||
@ -1602,6 +1602,7 @@ def test_to_json(default_conf, fee):
|
|||||||
'max_rate': None,
|
'max_rate': None,
|
||||||
'strategy': None,
|
'strategy': None,
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
|
'enter_tag': None,
|
||||||
'timeframe': None,
|
'timeframe': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
'leverage': None,
|
'leverage': None,
|
||||||
@ -1624,7 +1625,7 @@ def test_to_json(default_conf, fee):
|
|||||||
close_date=arrow.utcnow().shift(hours=-1).datetime,
|
close_date=arrow.utcnow().shift(hours=-1).datetime,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
close_rate=0.125,
|
close_rate=0.125,
|
||||||
buy_tag='buys_signal_001',
|
enter_tag='buys_signal_001',
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
)
|
)
|
||||||
result = trade.to_json()
|
result = trade.to_json()
|
||||||
@ -1675,6 +1676,7 @@ def test_to_json(default_conf, fee):
|
|||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'strategy': None,
|
'strategy': None,
|
||||||
'buy_tag': 'buys_signal_001',
|
'buy_tag': 'buys_signal_001',
|
||||||
|
'enter_tag': 'buys_signal_001',
|
||||||
'timeframe': None,
|
'timeframe': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
'leverage': None,
|
'leverage': None,
|
||||||
@ -2116,7 +2118,7 @@ def test_Trade_object_idem():
|
|||||||
'get_open_order_trades',
|
'get_open_order_trades',
|
||||||
'get_trades',
|
'get_trades',
|
||||||
'get_sell_reason_performance',
|
'get_sell_reason_performance',
|
||||||
'get_buy_tag_performance',
|
'get_enter_tag_performance',
|
||||||
'get_mix_tag_performance',
|
'get_mix_tag_performance',
|
||||||
|
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user