Merge branch 'develop' into backtest_live_models
This commit is contained in:
commit
93fe2b6446
BIN
docs/assets/tensorboard.jpg
Normal file
BIN
docs/assets/tensorboard.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 362 KiB |
@ -215,16 +215,18 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
|
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
|
||||||
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `True`.<br> **Datatype:** boolean
|
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `True`.<br> **Datatype:** boolean
|
||||||
| `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.<br> **Datatype:** dictionary
|
| `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.<br> **Datatype:** dictionary
|
||||||
|
| `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function. <br> **Datatype:** Boolean
|
||||||
| | **Webhook**
|
| | **Webhook**
|
||||||
| `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean
|
| `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean
|
||||||
| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `webhook.webhookentry` | Payload to send on entry. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.entry` | Payload to send on entry. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `webhook.webhookentrycancel` | Payload to send on entry order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.entry_cancel` | Payload to send on entry order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `webhook.webhookentryfill` | Payload to send on entry order filled. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.entry_fill` | Payload to send on entry order filled. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `webhook.webhookexit` | Payload to send on exit. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.exit` | Payload to send on exit. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `webhook.webhookexitcancel` | Payload to send on exit order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.exit_cancel` | Payload to send on exit order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `webhook.webhookexitfill` | Payload to send on exit order filled. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.exit_fill` | Payload to send on exit order filled. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.status` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
|
| `webhook.allow_custom_messages` | Enable the sending of Webhook messages from strategies via the dataprovider.send_msg() function. <br> **Datatype:** Boolean
|
||||||
| | **Rest API / FreqUI / Producer-Consumer**
|
| | **Rest API / FreqUI / Producer-Consumer**
|
||||||
| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** Boolean
|
| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** Boolean
|
||||||
| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** IPv4
|
| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** IPv4
|
||||||
|
@ -66,11 +66,11 @@ We will keep a compatibility layer for 1-2 versions (so both `buy_tag` and `ente
|
|||||||
|
|
||||||
#### Naming changes
|
#### Naming changes
|
||||||
|
|
||||||
Webhook terminology changed from "sell" to "exit", and from "buy" to "entry".
|
Webhook terminology changed from "sell" to "exit", and from "buy" to "entry", removing "webhook" in the process.
|
||||||
|
|
||||||
* `webhookbuy` -> `webhookentry`
|
* `webhookbuy`, `webhookentry` -> `entry`
|
||||||
* `webhookbuyfill` -> `webhookentryfill`
|
* `webhookbuyfill`, `webhookentryfill` -> `entry_fill`
|
||||||
* `webhookbuycancel` -> `webhookentrycancel`
|
* `webhookbuycancel`, `webhookentrycancel` -> `entry_cancel`
|
||||||
* `webhooksell` -> `webhookexit`
|
* `webhooksell`, `webhookexit` -> `exit`
|
||||||
* `webhooksellfill` -> `webhookexitfill`
|
* `webhooksellfill`, `webhookexitfill` -> `exit_fill`
|
||||||
* `webhooksellcancel` -> `webhookexitcancel`
|
* `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel`
|
||||||
|
@ -142,6 +142,20 @@ dataframe['outlier'] = np.where(dataframe['DI_values'] > self.di_max.value/10, 1
|
|||||||
|
|
||||||
This specific hyperopt would help you understand the appropriate `DI_values` for your particular parameter space.
|
This specific hyperopt would help you understand the appropriate `DI_values` for your particular parameter space.
|
||||||
|
|
||||||
|
## Using Tensorboard
|
||||||
|
|
||||||
|
Catboost models benefit from tracking training metrics via Tensorboard. You can take advantage of the FreqAI integration to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd freqtrade
|
||||||
|
tensorboard --logdir user_data/models/unique-id
|
||||||
|
```
|
||||||
|
|
||||||
|
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell if the user wishes to view the output in their browser at 127.0.0.1:6060 (6060 is the default port used by Tensorboard).
|
||||||
|
|
||||||
|
![tensorboard](assets/tensorboard.jpg)
|
||||||
|
|
||||||
|
|
||||||
## Setting up a follower
|
## Setting up a follower
|
||||||
|
|
||||||
You can indicate to the bot that it should not train models, but instead should look for models trained by a leader with a specific `identifier` by defining:
|
You can indicate to the bot that it should not train models, but instead should look for models trained by a leader with a specific `identifier` by defining:
|
||||||
|
@ -43,19 +43,25 @@ Note : `forcesell`, `forcebuy`, `emergencysell` are changed to `force_exit`, `fo
|
|||||||
* `order_time_in_force` buy -> entry, sell -> exit.
|
* `order_time_in_force` buy -> entry, sell -> exit.
|
||||||
* `order_types` buy -> entry, sell -> exit.
|
* `order_types` buy -> entry, sell -> exit.
|
||||||
* `unfilledtimeout` buy -> entry, sell -> exit.
|
* `unfilledtimeout` buy -> entry, sell -> exit.
|
||||||
|
* `ignore_buying_expired_candle_after` -> moved to root level instead of "ask_strategy/exit_pricing"
|
||||||
* Terminology changes
|
* Terminology changes
|
||||||
* Sell reasons changed to reflect the new naming of "exit" instead of sells. Be careful in your strategy if you're using `exit_reason` checks and eventually update your strategy.
|
* Sell reasons changed to reflect the new naming of "exit" instead of sells. Be careful in your strategy if you're using `exit_reason` checks and eventually update your strategy.
|
||||||
* `sell_signal` -> `exit_signal`
|
* `sell_signal` -> `exit_signal`
|
||||||
* `custom_sell` -> `custom_exit`
|
* `custom_sell` -> `custom_exit`
|
||||||
* `force_sell` -> `force_exit`
|
* `force_sell` -> `force_exit`
|
||||||
* `emergency_sell` -> `emergency_exit`
|
* `emergency_sell` -> `emergency_exit`
|
||||||
|
* Order pricing
|
||||||
|
* `bid_strategy` -> `entry_pricing`
|
||||||
|
* `ask_strategy` -> `exit_pricing`
|
||||||
|
* `ask_last_balance` -> `price_last_balance`
|
||||||
|
* `bid_last_balance` -> `price_last_balance`
|
||||||
* Webhook terminology changed from "sell" to "exit", and from "buy" to entry
|
* Webhook terminology changed from "sell" to "exit", and from "buy" to entry
|
||||||
* `webhookbuy` -> `webhookentry`
|
* `webhookbuy` -> `entry`
|
||||||
* `webhookbuyfill` -> `webhookentryfill`
|
* `webhookbuyfill` -> `entry_fill`
|
||||||
* `webhookbuycancel` -> `webhookentrycancel`
|
* `webhookbuycancel` -> `entry_cancel`
|
||||||
* `webhooksell` -> `webhookexit`
|
* `webhooksell` -> `exit`
|
||||||
* `webhooksellfill` -> `webhookexitfill`
|
* `webhooksellfill` -> `exit_fill`
|
||||||
* `webhooksellcancel` -> `webhookexitcancel`
|
* `webhooksellcancel` -> `exit_cancel`
|
||||||
* Telegram notification settings
|
* Telegram notification settings
|
||||||
* `buy` -> `entry`
|
* `buy` -> `entry`
|
||||||
* `buy_fill` -> `entry_fill`
|
* `buy_fill` -> `entry_fill`
|
||||||
@ -443,6 +449,7 @@ Please refer to the [pricing documentation](configuration.md#prices-used-for-ord
|
|||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"order_book_top": 1,
|
"order_book_top": 1,
|
||||||
"bid_last_balance": 0.0
|
"bid_last_balance": 0.0
|
||||||
|
"ignore_buying_expired_candle_after": 120
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -466,6 +473,7 @@ after:
|
|||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"order_book_top": 1,
|
"order_book_top": 1,
|
||||||
"price_last_balance": 0.0
|
"price_last_balance": 0.0
|
||||||
}
|
},
|
||||||
|
"ignore_buying_expired_candle_after": 120
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -77,6 +77,7 @@ Example configuration showing the different settings:
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
"chat_id": "your_telegram_chat_id",
|
"chat_id": "your_telegram_chat_id",
|
||||||
|
"allow_custom_messages": true,
|
||||||
"notification_settings": {
|
"notification_settings": {
|
||||||
"status": "silent",
|
"status": "silent",
|
||||||
"warning": "on",
|
"warning": "on",
|
||||||
@ -115,6 +116,7 @@ Example configuration showing the different settings:
|
|||||||
`show_candle` - show candle values as part of entry/exit messages. Only possible values are `"ohlc"` or `"off"`.
|
`show_candle` - show candle values as part of entry/exit messages. Only possible values are `"ohlc"` or `"off"`.
|
||||||
|
|
||||||
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
||||||
|
`allow_custom_messages` completely disable strategy messages.
|
||||||
`reload` allows you to disable reload-buttons on selected messages.
|
`reload` allows you to disable reload-buttons on selected messages.
|
||||||
|
|
||||||
## Create a custom keyboard (command shortcut buttons)
|
## Create a custom keyboard (command shortcut buttons)
|
||||||
|
@ -10,37 +10,37 @@ Sample configuration (tested using IFTTT).
|
|||||||
"webhook": {
|
"webhook": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "https://maker.ifttt.com/trigger/<YOUREVENT>/with/key/<YOURKEY>/",
|
"url": "https://maker.ifttt.com/trigger/<YOUREVENT>/with/key/<YOURKEY>/",
|
||||||
"webhookentry": {
|
"entry": {
|
||||||
"value1": "Buying {pair}",
|
"value1": "Buying {pair}",
|
||||||
"value2": "limit {limit:8f}",
|
"value2": "limit {limit:8f}",
|
||||||
"value3": "{stake_amount:8f} {stake_currency}"
|
"value3": "{stake_amount:8f} {stake_currency}"
|
||||||
},
|
},
|
||||||
"webhookentrycancel": {
|
"entry_cancel": {
|
||||||
"value1": "Cancelling Open Buy Order for {pair}",
|
"value1": "Cancelling Open Buy Order for {pair}",
|
||||||
"value2": "limit {limit:8f}",
|
"value2": "limit {limit:8f}",
|
||||||
"value3": "{stake_amount:8f} {stake_currency}"
|
"value3": "{stake_amount:8f} {stake_currency}"
|
||||||
},
|
},
|
||||||
"webhookentryfill": {
|
"entry_fill": {
|
||||||
"value1": "Buy Order for {pair} filled",
|
"value1": "Buy Order for {pair} filled",
|
||||||
"value2": "at {open_rate:8f}",
|
"value2": "at {open_rate:8f}",
|
||||||
"value3": ""
|
"value3": ""
|
||||||
},
|
},
|
||||||
"webhookexit": {
|
"exit": {
|
||||||
"value1": "Exiting {pair}",
|
"value1": "Exiting {pair}",
|
||||||
"value2": "limit {limit:8f}",
|
"value2": "limit {limit:8f}",
|
||||||
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
|
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
|
||||||
},
|
},
|
||||||
"webhookexitcancel": {
|
"exit_cancel": {
|
||||||
"value1": "Cancelling Open Exit Order for {pair}",
|
"value1": "Cancelling Open Exit Order for {pair}",
|
||||||
"value2": "limit {limit:8f}",
|
"value2": "limit {limit:8f}",
|
||||||
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
|
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
|
||||||
},
|
},
|
||||||
"webhookexitfill": {
|
"exit_fill": {
|
||||||
"value1": "Exit Order for {pair} filled",
|
"value1": "Exit Order for {pair} filled",
|
||||||
"value2": "at {close_rate:8f}.",
|
"value2": "at {close_rate:8f}.",
|
||||||
"value3": ""
|
"value3": ""
|
||||||
},
|
},
|
||||||
"webhookstatus": {
|
"status": {
|
||||||
"value1": "Status: {status}",
|
"value1": "Status: {status}",
|
||||||
"value2": "",
|
"value2": "",
|
||||||
"value3": ""
|
"value3": ""
|
||||||
@ -57,7 +57,7 @@ You can set the POST body format to Form-Encoded (default), JSON-Encoded, or raw
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "https://<YOURSUBDOMAIN>.cloud.mattermost.com/hooks/<YOURHOOK>",
|
"url": "https://<YOURSUBDOMAIN>.cloud.mattermost.com/hooks/<YOURHOOK>",
|
||||||
"format": "json",
|
"format": "json",
|
||||||
"webhookstatus": {
|
"status": {
|
||||||
"text": "Status: {status}"
|
"text": "Status: {status}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -88,17 +88,30 @@ Optional parameters are available to enable automatic retries for webhook messag
|
|||||||
"url": "https://<YOURHOOKURL>",
|
"url": "https://<YOURHOOKURL>",
|
||||||
"retries": 3,
|
"retries": 3,
|
||||||
"retry_delay": 0.2,
|
"retry_delay": 0.2,
|
||||||
"webhookstatus": {
|
"status": {
|
||||||
"status": "Status: {status}"
|
"status": "Status: {status}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Custom messages can be sent to Webhook endpoints via the `self.dp.send_msg()` function from within the strategy. To enable this, set the `allow_custom_messages` option to `true`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"webhook": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "https://<YOURHOOKURL>",
|
||||||
|
"allow_custom_messages": true,
|
||||||
|
"strategy_msg": {
|
||||||
|
"status": "StrategyMessage: {msg}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
### Webhookentry
|
### Entry
|
||||||
|
|
||||||
The fields in `webhook.webhookentry` are filled when the bot executes a long/short. Parameters are filled using string.format.
|
The fields in `webhook.entry` are filled when the bot executes a long/short. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
* `trade_id`
|
* `trade_id`
|
||||||
@ -118,9 +131,9 @@ Possible parameters are:
|
|||||||
* `current_rate`
|
* `current_rate`
|
||||||
* `enter_tag`
|
* `enter_tag`
|
||||||
|
|
||||||
### Webhookentrycancel
|
### Entry cancel
|
||||||
|
|
||||||
The fields in `webhook.webhookentrycancel` are filled when the bot cancels a long/short order. Parameters are filled using string.format.
|
The fields in `webhook.entry_cancel` are filled when the bot cancels a long/short order. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
* `trade_id`
|
* `trade_id`
|
||||||
@ -139,9 +152,9 @@ Possible parameters are:
|
|||||||
* `current_rate`
|
* `current_rate`
|
||||||
* `enter_tag`
|
* `enter_tag`
|
||||||
|
|
||||||
### Webhookentryfill
|
### Entry fill
|
||||||
|
|
||||||
The fields in `webhook.webhookentryfill` are filled when the bot filled a long/short order. Parameters are filled using string.format.
|
The fields in `webhook.entry_fill` are filled when the bot filled a long/short order. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
* `trade_id`
|
* `trade_id`
|
||||||
@ -160,9 +173,9 @@ Possible parameters are:
|
|||||||
* `current_rate`
|
* `current_rate`
|
||||||
* `enter_tag`
|
* `enter_tag`
|
||||||
|
|
||||||
### Webhookexit
|
### Exit
|
||||||
|
|
||||||
The fields in `webhook.webhookexit` are filled when the bot exits a trade. Parameters are filled using string.format.
|
The fields in `webhook.exit` are filled when the bot exits a trade. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
* `trade_id`
|
* `trade_id`
|
||||||
@ -184,9 +197,9 @@ Possible parameters are:
|
|||||||
* `open_date`
|
* `open_date`
|
||||||
* `close_date`
|
* `close_date`
|
||||||
|
|
||||||
### Webhookexitfill
|
### Exit fill
|
||||||
|
|
||||||
The fields in `webhook.webhookexitfill` are filled when the bot fills a exit order (closes a Trade). Parameters are filled using string.format.
|
The fields in `webhook.exit_fill` are filled when the bot fills a exit order (closes a Trade). Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
* `trade_id`
|
* `trade_id`
|
||||||
@ -209,9 +222,9 @@ Possible parameters are:
|
|||||||
* `open_date`
|
* `open_date`
|
||||||
* `close_date`
|
* `close_date`
|
||||||
|
|
||||||
### Webhookexitcancel
|
### Exit cancel
|
||||||
|
|
||||||
The fields in `webhook.webhookexitcancel` are filled when the bot cancels a exit order. Parameters are filled using string.format.
|
The fields in `webhook.exit_cancel` are filled when the bot cancels a exit order. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
* `trade_id`
|
* `trade_id`
|
||||||
@ -234,9 +247,9 @@ Possible parameters are:
|
|||||||
* `open_date`
|
* `open_date`
|
||||||
* `close_date`
|
* `close_date`
|
||||||
|
|
||||||
### Webhookstatus
|
### Status
|
||||||
|
|
||||||
The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.
|
The fields in `webhook.status` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.
|
||||||
|
|
||||||
The only possible value here is `{status}`.
|
The only possible value here is `{status}`.
|
||||||
|
|
||||||
@ -280,7 +293,6 @@ You can configure this as follows:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
The above represents the default (`exit_fill` and `entry_fill` are optional and will default to the above configuration) - modifications are obviously possible.
|
The above represents the default (`exit_fill` and `entry_fill` are optional and will default to the above configuration) - modifications are obviously possible.
|
||||||
|
|
||||||
Available fields correspond to the fields for webhooks and are documented in the corresponding webhook sections.
|
Available fields correspond to the fields for webhooks and are documented in the corresponding webhook sections.
|
||||||
@ -288,3 +300,13 @@ Available fields correspond to the fields for webhooks and are documented in the
|
|||||||
The notifications will look as follows by default.
|
The notifications will look as follows by default.
|
||||||
|
|
||||||
![discord-notification](assets/discord_notification.png)
|
![discord-notification](assets/discord_notification.png)
|
||||||
|
|
||||||
|
Custom messages can be sent from a strategy to Discord endpoints via the dataprovider.send_msg() function. To enable this, set the `allow_custom_messages` option to `true`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"discord": {
|
||||||
|
"enabled": true,
|
||||||
|
"webhook_url": "https://discord.com/api/webhooks/<Your webhook URL ...>",
|
||||||
|
"allow_custom_messages": true,
|
||||||
|
},
|
||||||
|
```
|
||||||
|
@ -5,7 +5,7 @@ bot constants
|
|||||||
"""
|
"""
|
||||||
from typing import Any, Dict, List, Literal, Tuple
|
from typing import Any, Dict, List, Literal, Tuple
|
||||||
|
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType, RPCMessageType
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = 'config.json'
|
DEFAULT_CONFIG = 'config.json'
|
||||||
@ -282,6 +282,7 @@ CONF_SCHEMA = {
|
|||||||
'enabled': {'type': 'boolean'},
|
'enabled': {'type': 'boolean'},
|
||||||
'token': {'type': 'string'},
|
'token': {'type': 'string'},
|
||||||
'chat_id': {'type': 'string'},
|
'chat_id': {'type': 'string'},
|
||||||
|
'allow_custom_messages': {'type': 'boolean', 'default': True},
|
||||||
'balance_dust_level': {'type': 'number', 'minimum': 0.0},
|
'balance_dust_level': {'type': 'number', 'minimum': 0.0},
|
||||||
'notification_settings': {
|
'notification_settings': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@ -344,6 +345,8 @@ CONF_SCHEMA = {
|
|||||||
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
|
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
|
||||||
'retries': {'type': 'integer', 'minimum': 0},
|
'retries': {'type': 'integer', 'minimum': 0},
|
||||||
'retry_delay': {'type': 'number', 'minimum': 0},
|
'retry_delay': {'type': 'number', 'minimum': 0},
|
||||||
|
**dict([(x, {'type': 'object'}) for x in RPCMessageType]),
|
||||||
|
# Below -> Deprecated
|
||||||
'webhookentry': {'type': 'object'},
|
'webhookentry': {'type': 'object'},
|
||||||
'webhookentrycancel': {'type': 'object'},
|
'webhookentrycancel': {'type': 'object'},
|
||||||
'webhookentryfill': {'type': 'object'},
|
'webhookentryfill': {'type': 'object'},
|
||||||
@ -653,5 +656,6 @@ LongShort = Literal['long', 'short']
|
|||||||
EntryExit = Literal['entry', 'exit']
|
EntryExit = Literal['entry', 'exit']
|
||||||
BuySell = Literal['buy', 'sell']
|
BuySell = Literal['buy', 'sell']
|
||||||
MakerTaker = Literal['maker', 'taker']
|
MakerTaker = Literal['maker', 'taker']
|
||||||
|
BidAsk = Literal['bid', 'ask']
|
||||||
|
|
||||||
Config = Dict[str, Any]
|
Config = Dict[str, Any]
|
||||||
|
@ -11,6 +11,7 @@ from freqtrade.enums import CandleType, MarginMode, TradingMode
|
|||||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.misc import deep_merge_dicts, json_load
|
from freqtrade.misc import deep_merge_dicts, json_load
|
||||||
|
|
||||||
|
|
||||||
@ -59,7 +60,7 @@ class Binance(Exchange):
|
|||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
|
||||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
|
||||||
tickers = super().get_tickers(symbols=symbols, cached=cached)
|
tickers = super().get_tickers(symbols=symbols, cached=cached)
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
# Binance's future result has no bid/ask values.
|
# Binance's future result has no bid/ask values.
|
||||||
|
@ -20,8 +20,8 @@ from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
|
|||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
from pandas import DataFrame, concat
|
from pandas import DataFrame, concat
|
||||||
|
|
||||||
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell,
|
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk,
|
||||||
Config, EntryExit, ListPairsWithTimeframes, MakerTaker,
|
BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker,
|
||||||
PairWithTimeframe)
|
PairWithTimeframe)
|
||||||
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
|
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
|
||||||
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
||||||
@ -31,6 +31,7 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun
|
|||||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
|
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
|
||||||
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
|
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
|
||||||
remove_credentials, retrier, retrier_async)
|
remove_credentials, retrier, retrier_async)
|
||||||
|
from freqtrade.exchange.types import Ticker, Tickers
|
||||||
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
|
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
|
||||||
safe_value_fallback2)
|
safe_value_fallback2)
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
@ -1420,14 +1421,17 @@ class Exchange:
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
|
||||||
"""
|
"""
|
||||||
:param cached: Allow cached result
|
:param cached: Allow cached result
|
||||||
:return: fetch_tickers result
|
:return: fetch_tickers result
|
||||||
"""
|
"""
|
||||||
|
tickers: Tickers
|
||||||
|
if not self.exchange_has('fetchTickers'):
|
||||||
|
return {}
|
||||||
if cached:
|
if cached:
|
||||||
with self._cache_lock:
|
with self._cache_lock:
|
||||||
tickers = self._fetch_tickers_cache.get('fetch_tickers')
|
tickers = self._fetch_tickers_cache.get('fetch_tickers') # type: ignore
|
||||||
if tickers:
|
if tickers:
|
||||||
return tickers
|
return tickers
|
||||||
try:
|
try:
|
||||||
@ -1450,12 +1454,12 @@ class Exchange:
|
|||||||
# Pricing info
|
# Pricing info
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def fetch_ticker(self, pair: str) -> dict:
|
def fetch_ticker(self, pair: str) -> Ticker:
|
||||||
try:
|
try:
|
||||||
if (pair not in self.markets or
|
if (pair not in self.markets or
|
||||||
self.markets[pair].get('active', False) is False):
|
self.markets[pair].get('active', False) is False):
|
||||||
raise ExchangeError(f"Pair {pair} not available")
|
raise ExchangeError(f"Pair {pair} not available")
|
||||||
data = self._api.fetch_ticker(pair)
|
data: Ticker = self._api.fetch_ticker(pair)
|
||||||
return data
|
return data
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
@ -1506,7 +1510,7 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def _get_price_side(self, side: str, is_short: bool, conf_strategy: Dict) -> str:
|
def _get_price_side(self, side: str, is_short: bool, conf_strategy: Dict) -> BidAsk:
|
||||||
price_side = conf_strategy['price_side']
|
price_side = conf_strategy['price_side']
|
||||||
|
|
||||||
if price_side in ('same', 'other'):
|
if price_side in ('same', 'other'):
|
||||||
@ -1525,7 +1529,7 @@ class Exchange:
|
|||||||
|
|
||||||
def get_rate(self, pair: str, refresh: bool,
|
def get_rate(self, pair: str, refresh: bool,
|
||||||
side: EntryExit, is_short: bool,
|
side: EntryExit, is_short: bool,
|
||||||
order_book: Optional[dict] = None, ticker: Optional[dict] = None) -> float:
|
order_book: Optional[dict] = None, ticker: Optional[Ticker] = None) -> float:
|
||||||
"""
|
"""
|
||||||
Calculates bid/ask target
|
Calculates bid/ask target
|
||||||
bid rate - between current ask price and last price
|
bid rate - between current ask price and last price
|
||||||
|
@ -12,6 +12,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali
|
|||||||
OperationalException, TemporaryError)
|
OperationalException, TemporaryError)
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
|
from freqtrade.exchange.types import Tickers
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -45,7 +46,7 @@ class Kraken(Exchange):
|
|||||||
return (parent_check and
|
return (parent_check and
|
||||||
market.get('darkpool', False) is False)
|
market.get('darkpool', False) is False)
|
||||||
|
|
||||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
|
||||||
# Only fetch tickers for current stake currency
|
# Only fetch tickers for current stake currency
|
||||||
# Otherwise the request for kraken becomes too large.
|
# Otherwise the request for kraken becomes too large.
|
||||||
symbols = list(self.get_markets(quote_currencies=[self._config['stake_currency']]))
|
symbols = list(self.get_markets(quote_currencies=[self._config['stake_currency']]))
|
||||||
|
16
freqtrade/exchange/types.py
Normal file
16
freqtrade/exchange/types.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from typing import Dict, Optional, TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class Ticker(TypedDict):
|
||||||
|
symbol: str
|
||||||
|
ask: Optional[float]
|
||||||
|
askVolume: Optional[float]
|
||||||
|
bid: Optional[float]
|
||||||
|
bidVolume: Optional[float]
|
||||||
|
last: Optional[float]
|
||||||
|
quoteVolume: Optional[float]
|
||||||
|
baseVolume: Optional[float]
|
||||||
|
# Several more - only listing required.
|
||||||
|
|
||||||
|
|
||||||
|
Tickers = Dict[str, Ticker]
|
@ -245,6 +245,7 @@ class FreqaiDataKitchen:
|
|||||||
self.data["filter_drop_index_training"] = drop_index
|
self.data["filter_drop_index_training"] = drop_index
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
filtered_df = self.check_pred_labels(filtered_df)
|
||||||
# we are backtesting so we need to preserve row number to send back to strategy,
|
# we are backtesting so we need to preserve row number to send back to strategy,
|
||||||
# so now we use do_predict to avoid any prediction based on a NaN
|
# so now we use do_predict to avoid any prediction based on a NaN
|
||||||
drop_index = pd.isnull(filtered_df).any(axis=1)
|
drop_index = pd.isnull(filtered_df).any(axis=1)
|
||||||
@ -487,6 +488,24 @@ class FreqaiDataKitchen:
|
|||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
def check_pred_labels(self, df_predictions: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Check that prediction feature labels match training feature labels.
|
||||||
|
:params:
|
||||||
|
:df_predictions: incoming predictions
|
||||||
|
"""
|
||||||
|
train_labels = self.data_dictionary["train_features"].columns
|
||||||
|
pred_labels = df_predictions.columns
|
||||||
|
num_diffs = len(pred_labels.difference(train_labels))
|
||||||
|
if num_diffs != 0:
|
||||||
|
df_predictions = df_predictions[train_labels]
|
||||||
|
logger.warning(
|
||||||
|
f"Removed {num_diffs} features from prediction features, "
|
||||||
|
f"these were likely considered constant values during most recent training."
|
||||||
|
)
|
||||||
|
|
||||||
|
return df_predictions
|
||||||
|
|
||||||
def principal_component_analysis(self) -> None:
|
def principal_component_analysis(self) -> None:
|
||||||
"""
|
"""
|
||||||
Performs Principal Component Analysis on the data for dimensionality reduction
|
Performs Principal Component Analysis on the data for dimensionality reduction
|
||||||
|
@ -200,16 +200,15 @@ class IFreqaiModel(ABC):
|
|||||||
(_, trained_timestamp, _) = self.dd.get_pair_dict_info(pair)
|
(_, trained_timestamp, _) = self.dd.get_pair_dict_info(pair)
|
||||||
|
|
||||||
dk = FreqaiDataKitchen(self.config, self.live, pair)
|
dk = FreqaiDataKitchen(self.config, self.live, pair)
|
||||||
dk.set_paths(pair, trained_timestamp)
|
|
||||||
(
|
(
|
||||||
retrain,
|
retrain,
|
||||||
new_trained_timerange,
|
new_trained_timerange,
|
||||||
data_load_timerange,
|
data_load_timerange,
|
||||||
) = dk.check_if_new_training_required(trained_timestamp)
|
) = dk.check_if_new_training_required(trained_timestamp)
|
||||||
dk.set_paths(pair, new_trained_timerange.stopts)
|
|
||||||
|
|
||||||
if retrain:
|
if retrain:
|
||||||
self.train_timer('start')
|
self.train_timer('start')
|
||||||
|
dk.set_paths(pair, new_trained_timerange.stopts)
|
||||||
try:
|
try:
|
||||||
self.extract_data_and_train_model(
|
self.extract_data_and_train_model(
|
||||||
new_trained_timerange, pair, strategy, dk, data_load_timerange
|
new_trained_timerange, pair, strategy, dk, data_load_timerange
|
||||||
@ -290,9 +289,7 @@ class IFreqaiModel(ABC):
|
|||||||
if dk.backtest_live_models:
|
if dk.backtest_live_models:
|
||||||
timestamp_model_id = int(tr_backtest.startts)
|
timestamp_model_id = int(tr_backtest.startts)
|
||||||
|
|
||||||
dk.data_path = Path(
|
dk.set_paths(pair, timestamp_model_id)
|
||||||
dk.full_path / f"sub-train-{pair.split('/')[0]}_{timestamp_model_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
dk.set_new_model_names(pair, timestamp_model_id)
|
dk.set_new_model_names(pair, timestamp_model_id)
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from catboost import CatBoostClassifier, Pool
|
from catboost import CatBoostClassifier, Pool
|
||||||
@ -31,8 +32,9 @@ class CatboostClassifier(BaseClassifierModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
cbr = CatBoostClassifier(
|
cbr = CatBoostClassifier(
|
||||||
allow_writing_files=False,
|
allow_writing_files=True,
|
||||||
loss_function='MultiClass',
|
loss_function='MultiClass',
|
||||||
|
train_dir=Path(dk.data_path),
|
||||||
**self.model_training_parameters,
|
**self.model_training_parameters,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from catboost import CatBoostRegressor, Pool
|
from catboost import CatBoostRegressor, Pool
|
||||||
@ -41,7 +42,8 @@ class CatboostRegressor(BaseRegressionModel):
|
|||||||
init_model = self.get_init_model(dk.pair)
|
init_model = self.get_init_model(dk.pair)
|
||||||
|
|
||||||
model = CatBoostRegressor(
|
model = CatBoostRegressor(
|
||||||
allow_writing_files=False,
|
allow_writing_files=True,
|
||||||
|
train_dir=Path(dk.data_path),
|
||||||
**self.model_training_parameters,
|
**self.model_training_parameters,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from catboost import CatBoostRegressor, Pool
|
from catboost import CatBoostRegressor, Pool
|
||||||
@ -26,7 +27,8 @@ class CatboostRegressorMultiTarget(BaseRegressionModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
cbr = CatBoostRegressor(
|
cbr = CatBoostRegressor(
|
||||||
allow_writing_files=False,
|
allow_writing_files=True,
|
||||||
|
train_dir=Path(dk.data_path),
|
||||||
**self.model_training_parameters,
|
**self.model_training_parameters,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterator, List
|
from typing import Any, Dict, Iterator, List, Mapping, Union
|
||||||
from typing.io import IO
|
from typing.io import IO
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@ -186,7 +186,10 @@ def safe_value_fallback(obj: dict, key1: str, key2: str, default_value=None):
|
|||||||
return default_value
|
return default_value
|
||||||
|
|
||||||
|
|
||||||
def safe_value_fallback2(dict1: dict, dict2: dict, key1: str, key2: str, default_value=None):
|
dictMap = Union[Dict[str, Any], Mapping[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
def safe_value_fallback2(dict1: dictMap, dict2: dictMap, key1: str, key2: str, default_value=None):
|
||||||
"""
|
"""
|
||||||
Search a value in dict1, return this if it's not None.
|
Search a value in dict1, return this if it's not None.
|
||||||
Fall back to dict2 - return key2 from dict2 if it's not None.
|
Fall back to dict2 - return key2 from dict2 if it's not None.
|
||||||
|
@ -10,6 +10,7 @@ from pandas import DataFrame
|
|||||||
|
|
||||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.misc import plural
|
from freqtrade.misc import plural
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
from freqtrade.util import PeriodicCache
|
from freqtrade.util import PeriodicCache
|
||||||
@ -67,7 +68,7 @@ class AgeFilter(IPairList):
|
|||||||
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
|
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
|
||||||
) if self._max_days_listed else '')
|
) if self._max_days_listed else '')
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
:param pairlist: pairlist to filter or sort
|
:param pairlist: pairlist to filter or sort
|
||||||
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
||||||
|
@ -4,11 +4,12 @@ PairList Handler base class
|
|||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod, abstractproperty
|
from abc import ABC, abstractmethod, abstractproperty
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import Exchange, market_is_active
|
from freqtrade.exchange import Exchange, market_is_active
|
||||||
|
from freqtrade.exchange.types import Ticker, Tickers
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
|
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ class IPairList(LoggingMixin, ABC):
|
|||||||
-> Please overwrite in subclasses
|
-> Please overwrite in subclasses
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
|
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||||
"""
|
"""
|
||||||
Check one pair against Pairlist Handler's specific conditions.
|
Check one pair against Pairlist Handler's specific conditions.
|
||||||
|
|
||||||
@ -74,7 +75,7 @@ class IPairList(LoggingMixin, ABC):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def gen_pairlist(self, tickers: Dict) -> List[str]:
|
def gen_pairlist(self, tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Generate the pairlist.
|
Generate the pairlist.
|
||||||
|
|
||||||
@ -91,7 +92,7 @@ class IPairList(LoggingMixin, ABC):
|
|||||||
raise OperationalException("This Pairlist Handler should not be used "
|
raise OperationalException("This Pairlist Handler should not be used "
|
||||||
"at the first position in the list of Pairlist Handlers.")
|
"at the first position in the list of Pairlist Handlers.")
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Filters and sorts pairlist and returns the whitelist again.
|
Filters and sorts pairlist and returns the whitelist again.
|
||||||
|
|
||||||
@ -110,7 +111,7 @@ class IPairList(LoggingMixin, ABC):
|
|||||||
# Copy list since we're modifying this list
|
# Copy list since we're modifying this list
|
||||||
for p in deepcopy(pairlist):
|
for p in deepcopy(pairlist):
|
||||||
# Filter out assets
|
# Filter out assets
|
||||||
if not self._validate_pair(p, tickers[p] if p in tickers else {}):
|
if not self._validate_pair(p, tickers[p] if p in tickers else None):
|
||||||
pairlist.remove(p)
|
pairlist.remove(p)
|
||||||
|
|
||||||
return pairlist
|
return pairlist
|
||||||
|
@ -6,6 +6,7 @@ from typing import Any, Dict, List
|
|||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ class OffsetFilter(IPairList):
|
|||||||
return f"{self.name} - Taking {self._number_pairs} Pairs, starting from {self._offset}."
|
return f"{self.name} - Taking {self._number_pairs} Pairs, starting from {self._offset}."
|
||||||
return f"{self.name} - Offsetting pairs by {self._offset}."
|
return f"{self.name} - Offsetting pairs by {self._offset}."
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Filters and sorts pairlist and returns the whitelist again.
|
Filters and sorts pairlist and returns the whitelist again.
|
||||||
Called on each bot iteration - please use internal caching if necessary
|
Called on each bot iteration - please use internal caching if necessary
|
||||||
|
@ -7,6 +7,7 @@ from typing import Any, Dict, List
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ class PerformanceFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
return f"{self.name} - Sorting pairs by performance."
|
return f"{self.name} - Sorting pairs by performance."
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Filters and sorts pairlist and returns the allowlist again.
|
Filters and sorts pairlist and returns the allowlist again.
|
||||||
Called on each bot iteration - please use internal caching if necessary
|
Called on each bot iteration - please use internal caching if necessary
|
||||||
|
@ -2,10 +2,11 @@
|
|||||||
Precision pair list filter
|
Precision pair list filter
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.exchange.types import Ticker
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ class PrecisionFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
return f"{self.name} - Filtering untradable pairs."
|
return f"{self.name} - Filtering untradable pairs."
|
||||||
|
|
||||||
def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
|
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very
|
Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very
|
||||||
low value pairs.
|
low value pairs.
|
||||||
@ -52,7 +53,7 @@ class PrecisionFilter(IPairList):
|
|||||||
:param ticker: ticker dict as returned from ccxt.fetch_ticker
|
:param ticker: ticker dict as returned from ccxt.fetch_ticker
|
||||||
:return: True if the pair can stay, false if it should be removed
|
:return: True if the pair can stay, false if it should be removed
|
||||||
"""
|
"""
|
||||||
if ticker.get('last', None) is None:
|
if not ticker or ticker.get('last', None) is None:
|
||||||
self.log_once(f"Removed {pair} from whitelist, because "
|
self.log_once(f"Removed {pair} from whitelist, because "
|
||||||
"ticker['last'] is empty (Usually no trade in the last 24h).",
|
"ticker['last'] is empty (Usually no trade in the last 24h).",
|
||||||
logger.info)
|
logger.info)
|
||||||
|
@ -2,10 +2,11 @@
|
|||||||
Price pair list filter
|
Price pair list filter
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.exchange.types import Ticker
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -64,14 +65,16 @@ class PriceFilter(IPairList):
|
|||||||
|
|
||||||
return f"{self.name} - No price filters configured."
|
return f"{self.name} - No price filters configured."
|
||||||
|
|
||||||
def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
|
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if if one price-step (pip) is > than a certain barrier.
|
Check if if one price-step (pip) is > than a certain barrier.
|
||||||
:param pair: Pair that's currently validated
|
:param pair: Pair that's currently validated
|
||||||
:param ticker: ticker dict as returned from ccxt.fetch_ticker
|
:param ticker: ticker dict as returned from ccxt.fetch_ticker
|
||||||
:return: True if the pair can stay, false if it should be removed
|
:return: True if the pair can stay, false if it should be removed
|
||||||
"""
|
"""
|
||||||
if ticker.get('last', None) is None or ticker.get('last') == 0:
|
if ticker and 'last' in ticker and ticker['last'] is not None and ticker.get('last') != 0:
|
||||||
|
price: float = ticker['last']
|
||||||
|
else:
|
||||||
self.log_once(f"Removed {pair} from whitelist, because "
|
self.log_once(f"Removed {pair} from whitelist, because "
|
||||||
"ticker['last'] is empty (Usually no trade in the last 24h).",
|
"ticker['last'] is empty (Usually no trade in the last 24h).",
|
||||||
logger.info)
|
logger.info)
|
||||||
@ -79,8 +82,8 @@ class PriceFilter(IPairList):
|
|||||||
|
|
||||||
# Perform low_price_ratio check.
|
# Perform low_price_ratio check.
|
||||||
if self._low_price_ratio != 0:
|
if self._low_price_ratio != 0:
|
||||||
compare = self._exchange.price_get_one_pip(pair, ticker['last'])
|
compare = self._exchange.price_get_one_pip(pair, price)
|
||||||
changeperc = compare / ticker['last']
|
changeperc = compare / price
|
||||||
if changeperc > self._low_price_ratio:
|
if changeperc > self._low_price_ratio:
|
||||||
self.log_once(f"Removed {pair} from whitelist, "
|
self.log_once(f"Removed {pair} from whitelist, "
|
||||||
f"because 1 unit is {changeperc:.3%}", logger.info)
|
f"because 1 unit is {changeperc:.3%}", logger.info)
|
||||||
@ -88,7 +91,6 @@ class PriceFilter(IPairList):
|
|||||||
|
|
||||||
# Perform low_amount check
|
# Perform low_amount check
|
||||||
if self._max_value != 0:
|
if self._max_value != 0:
|
||||||
price = ticker['last']
|
|
||||||
market = self._exchange.markets[pair]
|
market = self._exchange.markets[pair]
|
||||||
limits = market['limits']
|
limits = market['limits']
|
||||||
if (limits['amount']['min'] is not None):
|
if (limits['amount']['min'] is not None):
|
||||||
@ -113,14 +115,14 @@ class PriceFilter(IPairList):
|
|||||||
|
|
||||||
# Perform min_price check.
|
# Perform min_price check.
|
||||||
if self._min_price != 0:
|
if self._min_price != 0:
|
||||||
if ticker['last'] < self._min_price:
|
if price < self._min_price:
|
||||||
self.log_once(f"Removed {pair} from whitelist, "
|
self.log_once(f"Removed {pair} from whitelist, "
|
||||||
f"because last price < {self._min_price:.8f}", logger.info)
|
f"because last price < {self._min_price:.8f}", logger.info)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Perform max_price check.
|
# Perform max_price check.
|
||||||
if self._max_price != 0:
|
if self._max_price != 0:
|
||||||
if ticker['last'] > self._max_price:
|
if price > self._max_price:
|
||||||
self.log_once(f"Removed {pair} from whitelist, "
|
self.log_once(f"Removed {pair} from whitelist, "
|
||||||
f"because last price > {self._max_price:.8f}", logger.info)
|
f"because last price > {self._max_price:.8f}", logger.info)
|
||||||
return False
|
return False
|
||||||
|
@ -7,6 +7,7 @@ import logging
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -68,7 +69,7 @@ class ProducerPairList(IPairList):
|
|||||||
|
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
def gen_pairlist(self, tickers: Dict) -> List[str]:
|
def gen_pairlist(self, tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Generate the pairlist
|
Generate the pairlist
|
||||||
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
||||||
@ -79,7 +80,7 @@ class ProducerPairList(IPairList):
|
|||||||
pairs = self._whitelist_for_active_markets(self.verify_whitelist(pairs, logger.info))
|
pairs = self._whitelist_for_active_markets(self.verify_whitelist(pairs, logger.info))
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Filters and sorts pairlist and returns the whitelist again.
|
Filters and sorts pairlist and returns the whitelist again.
|
||||||
Called on each bot iteration - please use internal caching if necessary
|
Called on each bot iteration - please use internal caching if necessary
|
||||||
|
@ -7,6 +7,7 @@ from typing import Any, Dict, List
|
|||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ class ShuffleFilter(IPairList):
|
|||||||
return (f"{self.name} - Shuffling pairs" +
|
return (f"{self.name} - Shuffling pairs" +
|
||||||
(f", seed = {self._seed}." if self._seed is not None else "."))
|
(f", seed = {self._seed}." if self._seed is not None else "."))
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Filters and sorts pairlist and returns the whitelist again.
|
Filters and sorts pairlist and returns the whitelist again.
|
||||||
Called on each bot iteration - please use internal caching if necessary
|
Called on each bot iteration - please use internal caching if necessary
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
Spread pair list filter
|
Spread pair list filter
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exchange.types import Ticker
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -22,12 +22,6 @@ class SpreadFilter(IPairList):
|
|||||||
self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005)
|
self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005)
|
||||||
self._enabled = self._max_spread_ratio != 0
|
self._enabled = self._max_spread_ratio != 0
|
||||||
|
|
||||||
if not self._exchange.exchange_has('fetchTickers'):
|
|
||||||
raise OperationalException(
|
|
||||||
'Exchange does not support fetchTickers, therefore SpreadFilter cannot be used.'
|
|
||||||
'Please edit your config and restart the bot.'
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -44,14 +38,14 @@ class SpreadFilter(IPairList):
|
|||||||
return (f"{self.name} - Filtering pairs with ask/bid diff above "
|
return (f"{self.name} - Filtering pairs with ask/bid diff above "
|
||||||
f"{self._max_spread_ratio:.2%}.")
|
f"{self._max_spread_ratio:.2%}.")
|
||||||
|
|
||||||
def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
|
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||||
"""
|
"""
|
||||||
Validate spread for the ticker
|
Validate spread for the ticker
|
||||||
:param pair: Pair that's currently validated
|
:param pair: Pair that's currently validated
|
||||||
:param ticker: ticker dict as returned from ccxt.fetch_ticker
|
:param ticker: ticker dict as returned from ccxt.fetch_ticker
|
||||||
:return: True if the pair can stay, false if it should be removed
|
:return: True if the pair can stay, false if it should be removed
|
||||||
"""
|
"""
|
||||||
if 'bid' in ticker and 'ask' in ticker and ticker['ask'] and ticker['bid']:
|
if ticker and 'bid' in ticker and 'ask' in ticker and ticker['ask'] and ticker['bid']:
|
||||||
spread = 1 - ticker['bid'] / ticker['ask']
|
spread = 1 - ticker['bid'] / ticker['ask']
|
||||||
if spread > self._max_spread_ratio:
|
if spread > self._max_spread_ratio:
|
||||||
self.log_once(f"Removed {pair} from whitelist, because spread "
|
self.log_once(f"Removed {pair} from whitelist, because spread "
|
||||||
|
@ -8,6 +8,7 @@ from copy import deepcopy
|
|||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ class StaticPairList(IPairList):
|
|||||||
"""
|
"""
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
|
|
||||||
def gen_pairlist(self, tickers: Dict) -> List[str]:
|
def gen_pairlist(self, tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Generate the pairlist
|
Generate the pairlist
|
||||||
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
||||||
@ -53,7 +54,7 @@ class StaticPairList(IPairList):
|
|||||||
return self._whitelist_for_active_markets(
|
return self._whitelist_for_active_markets(
|
||||||
self.verify_whitelist(self._config['exchange']['pair_whitelist'], logger.info))
|
self.verify_whitelist(self._config['exchange']['pair_whitelist'], logger.info))
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Filters and sorts pairlist and returns the whitelist again.
|
Filters and sorts pairlist and returns the whitelist again.
|
||||||
Called on each bot iteration - please use internal caching if necessary
|
Called on each bot iteration - please use internal caching if necessary
|
||||||
|
@ -13,6 +13,7 @@ from pandas import DataFrame
|
|||||||
|
|
||||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.misc import plural
|
from freqtrade.misc import plural
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ class VolatilityFilter(IPairList):
|
|||||||
f"{self._min_volatility}-{self._max_volatility} "
|
f"{self._min_volatility}-{self._max_volatility} "
|
||||||
f" the last {self._days} {plural(self._days, 'day')}.")
|
f" the last {self._days} {plural(self._days, 'day')}.")
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Validate trading range
|
Validate trading range
|
||||||
:param pairlist: pairlist to filter or sort
|
:param pairlist: pairlist to filter or sort
|
||||||
|
@ -5,13 +5,14 @@ Provides dynamic pair list based on trade volumes
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Literal
|
||||||
|
|
||||||
from cachetools import TTLCache
|
from cachetools import TTLCache
|
||||||
|
|
||||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||||
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.misc import format_ms_time
|
from freqtrade.misc import format_ms_time
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ class VolumePairList(IPairList):
|
|||||||
|
|
||||||
self._stake_currency = config['stake_currency']
|
self._stake_currency = config['stake_currency']
|
||||||
self._number_pairs = self._pairlistconfig['number_assets']
|
self._number_pairs = self._pairlistconfig['number_assets']
|
||||||
self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume')
|
self._sort_key: Literal['quoteVolume'] = self._pairlistconfig.get('sort_key', 'quoteVolume')
|
||||||
self._min_value = self._pairlistconfig.get('min_value', 0)
|
self._min_value = self._pairlistconfig.get('min_value', 0)
|
||||||
self._refresh_period = self._pairlistconfig.get('refresh_period', 1800)
|
self._refresh_period = self._pairlistconfig.get('refresh_period', 1800)
|
||||||
self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
|
self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
|
||||||
@ -110,7 +111,7 @@ class VolumePairList(IPairList):
|
|||||||
"""
|
"""
|
||||||
return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs."
|
return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs."
|
||||||
|
|
||||||
def gen_pairlist(self, tickers: Dict) -> List[str]:
|
def gen_pairlist(self, tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Generate the pairlist
|
Generate the pairlist
|
||||||
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
||||||
|
@ -11,6 +11,7 @@ from pandas import DataFrame
|
|||||||
|
|
||||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.misc import plural
|
from freqtrade.misc import plural
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ class RangeStabilityFilter(IPairList):
|
|||||||
f"{self._min_rate_of_change}{max_rate_desc} over the "
|
f"{self._min_rate_of_change}{max_rate_desc} over the "
|
||||||
f"last {plural(self._days, 'day')}.")
|
f"last {plural(self._days, 'day')}.")
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Validate trading range
|
Validate trading range
|
||||||
:param pairlist: pairlist to filter or sort
|
:param pairlist: pairlist to filter or sort
|
||||||
|
@ -11,6 +11,7 @@ from freqtrade.constants import Config, ListPairsWithTimeframes
|
|||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
@ -45,6 +46,15 @@ class PairListManager(LoggingMixin):
|
|||||||
if not self._pairlist_handlers:
|
if not self._pairlist_handlers:
|
||||||
raise OperationalException("No Pairlist Handlers defined")
|
raise OperationalException("No Pairlist Handlers defined")
|
||||||
|
|
||||||
|
if self._tickers_needed and not self._exchange.exchange_has('fetchTickers'):
|
||||||
|
invalid = ". ".join([p.name for p in self._pairlist_handlers if p.needstickers])
|
||||||
|
|
||||||
|
raise OperationalException(
|
||||||
|
"Exchange does not support fetchTickers, therefore the following pairlists "
|
||||||
|
"cannot be used. Please edit your config and restart the bot.\n"
|
||||||
|
f"{invalid}."
|
||||||
|
)
|
||||||
|
|
||||||
refresh_period = config.get('pairlist_refresh_period', 3600)
|
refresh_period = config.get('pairlist_refresh_period', 3600)
|
||||||
LoggingMixin.__init__(self, logger, refresh_period)
|
LoggingMixin.__init__(self, logger, refresh_period)
|
||||||
|
|
||||||
@ -76,7 +86,7 @@ class PairListManager(LoggingMixin):
|
|||||||
return [{p.name: p.short_desc()} for p in self._pairlist_handlers]
|
return [{p.name: p.short_desc()} for p in self._pairlist_handlers]
|
||||||
|
|
||||||
@cached(TTLCache(maxsize=1, ttl=1800))
|
@cached(TTLCache(maxsize=1, ttl=1800))
|
||||||
def _get_cached_tickers(self):
|
def _get_cached_tickers(self) -> Tickers:
|
||||||
return self._exchange.get_tickers()
|
return self._exchange.get_tickers()
|
||||||
|
|
||||||
def refresh_pairlist(self) -> None:
|
def refresh_pairlist(self) -> None:
|
||||||
|
@ -4,6 +4,7 @@ from typing import Any, Dict
|
|||||||
from fastapi import APIRouter, Depends, WebSocketDisconnect
|
from fastapi import APIRouter, Depends, WebSocketDisconnect
|
||||||
from fastapi.websockets import WebSocket, WebSocketState
|
from fastapi.websockets import WebSocket, WebSocketState
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
from websockets.exceptions import WebSocketException
|
||||||
|
|
||||||
from freqtrade.enums import RPCMessageType, RPCRequestType
|
from freqtrade.enums import RPCMessageType, RPCRequestType
|
||||||
from freqtrade.rpc.api_server.api_auth import validate_ws_token
|
from freqtrade.rpc.api_server.api_auth import validate_ws_token
|
||||||
@ -102,7 +103,6 @@ async def message_endpoint(
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
channel = await channel_manager.on_connect(ws)
|
channel = await channel_manager.on_connect(ws)
|
||||||
|
|
||||||
if await is_websocket_alive(ws):
|
if await is_websocket_alive(ws):
|
||||||
|
|
||||||
logger.info(f"Consumer connected - {channel}")
|
logger.info(f"Consumer connected - {channel}")
|
||||||
@ -115,26 +115,31 @@ async def message_endpoint(
|
|||||||
# Process the request here
|
# Process the request here
|
||||||
await _process_consumer_request(request, channel, rpc)
|
await _process_consumer_request(request, channel, rpc)
|
||||||
|
|
||||||
except WebSocketDisconnect:
|
except (WebSocketDisconnect, WebSocketException):
|
||||||
# Handle client disconnects
|
# Handle client disconnects
|
||||||
logger.info(f"Consumer disconnected - {channel}")
|
logger.info(f"Consumer disconnected - {channel}")
|
||||||
await channel_manager.on_disconnect(ws)
|
except RuntimeError:
|
||||||
except Exception as e:
|
|
||||||
logger.info(f"Consumer connection failed - {channel}")
|
|
||||||
logger.exception(e)
|
|
||||||
# Handle cases like -
|
# Handle cases like -
|
||||||
# RuntimeError('Cannot call "send" once a closed message has been sent')
|
# RuntimeError('Cannot call "send" once a closed message has been sent')
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f"Consumer connection failed - {channel}: {e}")
|
||||||
|
logger.debug(e, exc_info=e)
|
||||||
|
finally:
|
||||||
await channel_manager.on_disconnect(ws)
|
await channel_manager.on_disconnect(ws)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
if channel:
|
||||||
|
await channel_manager.on_disconnect(ws)
|
||||||
await ws.close()
|
await ws.close()
|
||||||
|
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
# WebSocket was closed
|
# WebSocket was closed
|
||||||
await channel_manager.on_disconnect(ws)
|
# Do nothing
|
||||||
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to serve - {ws.client}")
|
logger.error(f"Failed to serve - {ws.client}")
|
||||||
# Log tracebacks to keep track of what errors are happening
|
# Log tracebacks to keep track of what errors are happening
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
finally:
|
||||||
await channel_manager.on_disconnect(ws)
|
await channel_manager.on_disconnect(ws)
|
||||||
|
@ -198,10 +198,6 @@ class ApiServer(RPCHandler):
|
|||||||
logger.debug(f"Found message of type: {message.get('type')}")
|
logger.debug(f"Found message of type: {message.get('type')}")
|
||||||
# Broadcast it
|
# Broadcast it
|
||||||
await self._ws_channel_manager.broadcast(message)
|
await self._ws_channel_manager.broadcast(message)
|
||||||
# Limit messages per sec.
|
|
||||||
# Could cause problems with queue size if too low, and
|
|
||||||
# problems with network traffik if too high.
|
|
||||||
await asyncio.sleep(0.001)
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -245,6 +241,7 @@ class ApiServer(RPCHandler):
|
|||||||
use_colors=False,
|
use_colors=False,
|
||||||
log_config=None,
|
log_config=None,
|
||||||
access_log=True if verbosity != 'error' else False,
|
access_log=True if verbosity != 'error' else False,
|
||||||
|
ws_ping_interval=None # We do this explicitly ourselves
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
self._server = UvicornServer(uvconfig)
|
self._server = UvicornServer(uvconfig)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
from typing import List, Optional, Type
|
from typing import Any, Dict, List, Optional, Type
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import WebSocket as FastAPIWebSocket
|
from fastapi import WebSocket as FastAPIWebSocket
|
||||||
@ -34,6 +35,8 @@ class WebSocketChannel:
|
|||||||
self._serializer_cls = serializer_cls
|
self._serializer_cls = serializer_cls
|
||||||
|
|
||||||
self._subscriptions: List[str] = []
|
self._subscriptions: List[str] = []
|
||||||
|
self.queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue(maxsize=32)
|
||||||
|
self._relay_task = asyncio.create_task(self.relay())
|
||||||
|
|
||||||
# Internal event to signify a closed websocket
|
# Internal event to signify a closed websocket
|
||||||
self._closed = False
|
self._closed = False
|
||||||
@ -48,12 +51,18 @@ class WebSocketChannel:
|
|||||||
def remote_addr(self):
|
def remote_addr(self):
|
||||||
return self._websocket.remote_addr
|
return self._websocket.remote_addr
|
||||||
|
|
||||||
async def send(self, data):
|
async def _send(self, data):
|
||||||
"""
|
"""
|
||||||
Send data on the wrapped websocket
|
Send data on the wrapped websocket
|
||||||
"""
|
"""
|
||||||
await self._wrapped_ws.send(data)
|
await self._wrapped_ws.send(data)
|
||||||
|
|
||||||
|
async def send(self, data):
|
||||||
|
"""
|
||||||
|
Add the data to the queue to be sent
|
||||||
|
"""
|
||||||
|
self.queue.put_nowait(data)
|
||||||
|
|
||||||
async def recv(self):
|
async def recv(self):
|
||||||
"""
|
"""
|
||||||
Receive data on the wrapped websocket
|
Receive data on the wrapped websocket
|
||||||
@ -72,6 +81,7 @@ class WebSocketChannel:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
self._closed = True
|
self._closed = True
|
||||||
|
self._relay_task.cancel()
|
||||||
|
|
||||||
def is_closed(self) -> bool:
|
def is_closed(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -95,6 +105,26 @@ class WebSocketChannel:
|
|||||||
"""
|
"""
|
||||||
return message_type in self._subscriptions
|
return message_type in self._subscriptions
|
||||||
|
|
||||||
|
async def relay(self):
|
||||||
|
"""
|
||||||
|
Relay messages from the channel's queue and send them out. This is started
|
||||||
|
as a task.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
message = await self.queue.get()
|
||||||
|
try:
|
||||||
|
await self._send(message)
|
||||||
|
self.queue.task_done()
|
||||||
|
|
||||||
|
# Limit messages per sec.
|
||||||
|
# Could cause problems with queue size if too low, and
|
||||||
|
# problems with network traffik if too high.
|
||||||
|
# 0.001 = 1000/s
|
||||||
|
await asyncio.sleep(0.001)
|
||||||
|
except RuntimeError:
|
||||||
|
# The connection was closed, just exit the task
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
class ChannelManager:
|
class ChannelManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -155,12 +185,12 @@ class ChannelManager:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
message_type = data.get('type')
|
message_type = data.get('type')
|
||||||
for websocket, channel in self.channels.copy().items():
|
for websocket, channel in self.channels.copy().items():
|
||||||
try:
|
if channel.subscribed_to(message_type):
|
||||||
if channel.subscribed_to(message_type):
|
if not channel.queue.full():
|
||||||
await channel.send(data)
|
await channel.send(data)
|
||||||
except RuntimeError:
|
else:
|
||||||
# Handle cannot send after close cases
|
logger.info(f"Channel {channel} is too far behind, disconnecting")
|
||||||
await self.on_disconnect(websocket)
|
await self.on_disconnect(websocket)
|
||||||
|
|
||||||
async def send_direct(self, channel, data):
|
async def send_direct(self, channel, data):
|
||||||
"""
|
"""
|
||||||
|
@ -11,13 +11,12 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class Discord(Webhook):
|
class Discord(Webhook):
|
||||||
def __init__(self, rpc: 'RPC', config: Config):
|
def __init__(self, rpc: 'RPC', config: Config):
|
||||||
# super().__init__(rpc, config)
|
self._config = config
|
||||||
self.rpc = rpc
|
self.rpc = rpc
|
||||||
self.config = config
|
|
||||||
self.strategy = config.get('strategy', '')
|
self.strategy = config.get('strategy', '')
|
||||||
self.timeframe = config.get('timeframe', '')
|
self.timeframe = config.get('timeframe', '')
|
||||||
|
|
||||||
self._url = self.config['discord']['webhook_url']
|
self._url = config['discord']['webhook_url']
|
||||||
self._format = 'json'
|
self._format = 'json'
|
||||||
self._retries = 1
|
self._retries = 1
|
||||||
self._retry_delay = 0.1
|
self._retry_delay = 0.1
|
||||||
@ -31,19 +30,21 @@ class Discord(Webhook):
|
|||||||
|
|
||||||
def send_msg(self, msg) -> None:
|
def send_msg(self, msg) -> None:
|
||||||
|
|
||||||
if msg['type'].value in self.config['discord']:
|
if msg['type'].value in self._config['discord']:
|
||||||
logger.info(f"Sending discord message: {msg}")
|
logger.info(f"Sending discord message: {msg}")
|
||||||
|
|
||||||
msg['strategy'] = self.strategy
|
msg['strategy'] = self.strategy
|
||||||
msg['timeframe'] = self.timeframe
|
msg['timeframe'] = self.timeframe
|
||||||
fields = self.config['discord'].get(msg['type'].value)
|
fields = self._config['discord'].get(msg['type'].value)
|
||||||
color = 0x0000FF
|
color = 0x0000FF
|
||||||
if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL):
|
if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL):
|
||||||
profit_ratio = msg.get('profit_ratio')
|
profit_ratio = msg.get('profit_ratio')
|
||||||
color = (0x00FF00 if profit_ratio > 0 else 0xFF0000)
|
color = (0x00FF00 if profit_ratio > 0 else 0xFF0000)
|
||||||
|
title = msg['type'].value
|
||||||
|
if 'pair' in msg:
|
||||||
|
title = f"Trade: {msg['pair']} {msg['type'].value}"
|
||||||
embeds = [{
|
embeds = [{
|
||||||
'title': f"Trade: {msg['pair']} {msg['type'].value}",
|
'title': title,
|
||||||
'color': color,
|
'color': color,
|
||||||
'fields': [],
|
'fields': [],
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ class Discord(Webhook):
|
|||||||
for f in fields:
|
for f in fields:
|
||||||
for k, v in f.items():
|
for k, v in f.items():
|
||||||
v = v.format(**msg)
|
v = v.format(**msg)
|
||||||
embeds[0]['fields'].append( # type: ignore
|
embeds[0]['fields'].append(
|
||||||
{'name': k, 'value': v, 'inline': True})
|
{'name': k, 'value': v, 'inline': True})
|
||||||
|
|
||||||
# Send the message to discord channel
|
# Send the message to discord channel
|
||||||
|
@ -62,7 +62,7 @@ class ExternalMessageConsumer:
|
|||||||
self.enabled = self._emc_config.get('enabled', False)
|
self.enabled = self._emc_config.get('enabled', False)
|
||||||
self.producers: List[Producer] = self._emc_config.get('producers', [])
|
self.producers: List[Producer] = self._emc_config.get('producers', [])
|
||||||
|
|
||||||
self.wait_timeout = self._emc_config.get('wait_timeout', 300) # in seconds
|
self.wait_timeout = self._emc_config.get('wait_timeout', 30) # in seconds
|
||||||
self.ping_timeout = self._emc_config.get('ping_timeout', 10) # in seconds
|
self.ping_timeout = self._emc_config.get('ping_timeout', 10) # in seconds
|
||||||
self.sleep_time = self._emc_config.get('sleep_time', 10) # in seconds
|
self.sleep_time = self._emc_config.get('sleep_time', 10) # in seconds
|
||||||
|
|
||||||
@ -174,6 +174,7 @@ class ExternalMessageConsumer:
|
|||||||
:param producer: Dictionary containing producer info
|
:param producer: Dictionary containing producer info
|
||||||
:param lock: An asyncio Lock
|
:param lock: An asyncio Lock
|
||||||
"""
|
"""
|
||||||
|
channel = None
|
||||||
while self._running:
|
while self._running:
|
||||||
try:
|
try:
|
||||||
host, port = producer['host'], producer['port']
|
host, port = producer['host'], producer['port']
|
||||||
@ -182,7 +183,11 @@ class ExternalMessageConsumer:
|
|||||||
ws_url = f"ws://{host}:{port}/api/v1/message/ws?token={token}"
|
ws_url = f"ws://{host}:{port}/api/v1/message/ws?token={token}"
|
||||||
|
|
||||||
# This will raise InvalidURI if the url is bad
|
# This will raise InvalidURI if the url is bad
|
||||||
async with websockets.connect(ws_url, max_size=self.message_size_limit) as ws:
|
async with websockets.connect(
|
||||||
|
ws_url,
|
||||||
|
max_size=self.message_size_limit,
|
||||||
|
ping_interval=None
|
||||||
|
) as ws:
|
||||||
channel = WebSocketChannel(ws, channel_id=name)
|
channel = WebSocketChannel(ws, channel_id=name)
|
||||||
|
|
||||||
logger.info(f"Producer connection success - {channel}")
|
logger.info(f"Producer connection success - {channel}")
|
||||||
@ -224,6 +229,10 @@ class ExternalMessageConsumer:
|
|||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if channel:
|
||||||
|
await channel.close()
|
||||||
|
|
||||||
async def _receive_messages(
|
async def _receive_messages(
|
||||||
self,
|
self,
|
||||||
channel: WebSocketChannel,
|
channel: WebSocketChannel,
|
||||||
|
@ -88,10 +88,13 @@ class RPCManager:
|
|||||||
"""
|
"""
|
||||||
while queue:
|
while queue:
|
||||||
msg = queue.popleft()
|
msg = queue.popleft()
|
||||||
self.send_msg({
|
logger.info('Sending rpc strategy_msg: %s', msg)
|
||||||
'type': RPCMessageType.STRATEGY_MSG,
|
for mod in self.registered_modules:
|
||||||
'msg': msg,
|
if mod._config.get(mod.name, {}).get('allow_custom_messages', False):
|
||||||
})
|
mod.send_msg({
|
||||||
|
'type': RPCMessageType.STRATEGY_MSG,
|
||||||
|
'msg': msg,
|
||||||
|
})
|
||||||
|
|
||||||
def startup_messages(self, config: Config, pairlist, protections) -> None:
|
def startup_messages(self, config: Config, pairlist, protections) -> None:
|
||||||
if config['dry_run']:
|
if config['dry_run']:
|
||||||
|
@ -3,7 +3,7 @@ This module manages webhook communication
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from requests import RequestException, post
|
from requests import RequestException, post
|
||||||
|
|
||||||
@ -41,36 +41,44 @@ class Webhook(RPCHandler):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _get_value_dict(self, msg: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
|
whconfig = self._config['webhook']
|
||||||
|
# Deprecated 2022.10 - only keep generic method.
|
||||||
|
if msg['type'] in [RPCMessageType.ENTRY]:
|
||||||
|
valuedict = whconfig.get('webhookentry')
|
||||||
|
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
|
||||||
|
valuedict = whconfig.get('webhookentrycancel')
|
||||||
|
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
|
||||||
|
valuedict = whconfig.get('webhookentryfill')
|
||||||
|
elif msg['type'] == RPCMessageType.EXIT:
|
||||||
|
valuedict = whconfig.get('webhookexit')
|
||||||
|
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
||||||
|
valuedict = whconfig.get('webhookexitfill')
|
||||||
|
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
|
||||||
|
valuedict = whconfig.get('webhookexitcancel')
|
||||||
|
elif msg['type'] in (RPCMessageType.STATUS,
|
||||||
|
RPCMessageType.STARTUP,
|
||||||
|
RPCMessageType.WARNING):
|
||||||
|
valuedict = whconfig.get('webhookstatus')
|
||||||
|
elif msg['type'].value in whconfig:
|
||||||
|
# Allow all types ...
|
||||||
|
valuedict = whconfig.get(msg['type'].value)
|
||||||
|
elif msg['type'] in (
|
||||||
|
RPCMessageType.PROTECTION_TRIGGER,
|
||||||
|
RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
|
||||||
|
RPCMessageType.WHITELIST,
|
||||||
|
RPCMessageType.ANALYZED_DF,
|
||||||
|
RPCMessageType.STRATEGY_MSG):
|
||||||
|
# Don't fail for non-implemented types
|
||||||
|
return None
|
||||||
|
return valuedict
|
||||||
|
|
||||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||||
""" Send a message to telegram channel """
|
""" Send a message to telegram channel """
|
||||||
try:
|
try:
|
||||||
whconfig = self._config['webhook']
|
|
||||||
if msg['type'] in [RPCMessageType.ENTRY]:
|
valuedict = self._get_value_dict(msg)
|
||||||
valuedict = whconfig.get('webhookentry')
|
|
||||||
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
|
|
||||||
valuedict = whconfig.get('webhookentrycancel')
|
|
||||||
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
|
|
||||||
valuedict = whconfig.get('webhookentryfill')
|
|
||||||
elif msg['type'] == RPCMessageType.EXIT:
|
|
||||||
valuedict = whconfig.get('webhookexit')
|
|
||||||
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
|
||||||
valuedict = whconfig.get('webhookexitfill')
|
|
||||||
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
|
|
||||||
valuedict = whconfig.get('webhookexitcancel')
|
|
||||||
elif msg['type'] in (RPCMessageType.STATUS,
|
|
||||||
RPCMessageType.STARTUP,
|
|
||||||
RPCMessageType.WARNING):
|
|
||||||
valuedict = whconfig.get('webhookstatus')
|
|
||||||
elif msg['type'] in (
|
|
||||||
RPCMessageType.PROTECTION_TRIGGER,
|
|
||||||
RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
|
|
||||||
RPCMessageType.WHITELIST,
|
|
||||||
RPCMessageType.ANALYZED_DF,
|
|
||||||
RPCMessageType.STRATEGY_MSG):
|
|
||||||
# Don't fail for non-implemented types
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
|
||||||
if not valuedict:
|
if not valuedict:
|
||||||
logger.info("Message type '%s' not configured for webhooks", msg['type'])
|
logger.info("Message type '%s' not configured for webhooks", msg['type'])
|
||||||
return
|
return
|
||||||
|
@ -49,7 +49,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
_ft_params_from_file: Dict
|
_ft_params_from_file: Dict
|
||||||
# associated minimal roi
|
# associated minimal roi
|
||||||
minimal_roi: Dict = {}
|
minimal_roi: Dict = {"0": 10.0}
|
||||||
|
|
||||||
# associated stoploss
|
# associated stoploss
|
||||||
stoploss: float
|
stoploss: float
|
||||||
|
@ -7,3 +7,4 @@ joblib==1.2.0
|
|||||||
catboost==1.1; platform_machine != 'aarch64'
|
catboost==1.1; platform_machine != 'aarch64'
|
||||||
lightgbm==3.3.2
|
lightgbm==3.3.2
|
||||||
xgboost==1.6.2
|
xgboost==1.6.2
|
||||||
|
tensorboard==2.10.1
|
||||||
|
@ -82,7 +82,7 @@ def readable_timedelta(delta):
|
|||||||
"""
|
"""
|
||||||
attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'microseconds']
|
attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'microseconds']
|
||||||
return ", ".join([
|
return ", ".join([
|
||||||
'%d %s' % (getattr(delta, attr), attr if getattr(delta, attr) > 1 else attr[:-1])
|
'%d %s' % (getattr(delta, attr), attr if getattr(delta, attr) > 0 else attr[:-1])
|
||||||
for attr in attrs if getattr(delta, attr)
|
for attr in attrs if getattr(delta, attr)
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ class ClientProtocol:
|
|||||||
|
|
||||||
def _calculate_time_difference(self):
|
def _calculate_time_difference(self):
|
||||||
old_last_received_at = self._LAST_RECEIVED_AT
|
old_last_received_at = self._LAST_RECEIVED_AT
|
||||||
self._LAST_RECEIVED_AT = time.time() * 1000
|
self._LAST_RECEIVED_AT = time.time() * 1e6
|
||||||
time_delta = relativedelta(microseconds=(self._LAST_RECEIVED_AT - old_last_received_at))
|
time_delta = relativedelta(microseconds=(self._LAST_RECEIVED_AT - old_last_received_at))
|
||||||
|
|
||||||
return readable_timedelta(time_delta)
|
return readable_timedelta(time_delta)
|
||||||
@ -238,7 +238,7 @@ async def create_client(
|
|||||||
|
|
||||||
except (
|
except (
|
||||||
asyncio.TimeoutError,
|
asyncio.TimeoutError,
|
||||||
websockets.exceptions.ConnectionClosed
|
websockets.exceptions.WebSocketException
|
||||||
):
|
):
|
||||||
# Try pinging
|
# Try pinging
|
||||||
try:
|
try:
|
||||||
@ -298,7 +298,7 @@ async def _main(args):
|
|||||||
producers = emc_config.get('producers', [])
|
producers = emc_config.get('producers', [])
|
||||||
producer = producers[0]
|
producer = producers[0]
|
||||||
|
|
||||||
wait_timeout = emc_config.get('wait_timeout', 300)
|
wait_timeout = emc_config.get('wait_timeout', 30)
|
||||||
ping_timeout = emc_config.get('ping_timeout', 10)
|
ping_timeout = emc_config.get('ping_timeout', 10)
|
||||||
sleep_time = emc_config.get('sleep_time', 10)
|
sleep_time = emc_config.get('sleep_time', 10)
|
||||||
message_size_limit = (emc_config.get('message_size_limit', 8) << 20)
|
message_size_limit = (emc_config.get('message_size_limit', 8) << 20)
|
||||||
@ -311,7 +311,8 @@ async def _main(args):
|
|||||||
sleep_time=sleep_time,
|
sleep_time=sleep_time,
|
||||||
ping_timeout=ping_timeout,
|
ping_timeout=ping_timeout,
|
||||||
wait_timeout=wait_timeout,
|
wait_timeout=wait_timeout,
|
||||||
max_size=message_size_limit
|
max_size=message_size_limit,
|
||||||
|
ping_interval=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ EXCHANGES = {
|
|||||||
'leverage_in_spot_market': True,
|
'leverage_in_spot_market': True,
|
||||||
},
|
},
|
||||||
'kucoin': {
|
'kucoin': {
|
||||||
'pair': 'BTC/USDT',
|
'pair': 'XRP/USDT',
|
||||||
'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
'hasQuoteVolume': True,
|
'hasQuoteVolume': True,
|
||||||
'timeframe': '5m',
|
'timeframe': '5m',
|
||||||
|
@ -1834,6 +1834,7 @@ def test_get_tickers(default_conf, mocker, exchange_name):
|
|||||||
'last': 41,
|
'last': 41,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mocker.patch('freqtrade.exchange.exchange.Exchange.exchange_has', return_value=True)
|
||||||
api_mock.fetch_tickers = MagicMock(return_value=tick)
|
api_mock.fetch_tickers = MagicMock(return_value=tick)
|
||||||
api_mock.fetch_bids_asks = MagicMock(return_value={})
|
api_mock.fetch_bids_asks = MagicMock(return_value={})
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
@ -1883,6 +1884,11 @@ def test_get_tickers(default_conf, mocker, exchange_name):
|
|||||||
assert api_mock.fetch_tickers.call_count == 1
|
assert api_mock.fetch_tickers.call_count == 1
|
||||||
assert api_mock.fetch_bids_asks.call_count == (1 if exchange_name == 'binance' else 0)
|
assert api_mock.fetch_bids_asks.call_count == (1 if exchange_name == 'binance' else 0)
|
||||||
|
|
||||||
|
api_mock.fetch_tickers.reset_mock()
|
||||||
|
api_mock.fetch_bids_asks.reset_mock()
|
||||||
|
mocker.patch('freqtrade.exchange.exchange.Exchange.exchange_has', return_value=False)
|
||||||
|
assert exchange.get_tickers() == {}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_fetch_ticker(default_conf, mocker, exchange_name):
|
def test_fetch_ticker(default_conf, mocker, exchange_name):
|
||||||
|
@ -107,6 +107,8 @@ def make_unfiltered_dataframe(mocker, freqai_conf):
|
|||||||
unfiltered_dataframe = freqai.dk.use_strategy_to_populate_indicators(
|
unfiltered_dataframe = freqai.dk.use_strategy_to_populate_indicators(
|
||||||
strategy, corr_dataframes, base_dataframes, freqai.dk.pair
|
strategy, corr_dataframes, base_dataframes, freqai.dk.pair
|
||||||
)
|
)
|
||||||
|
for i in range(5):
|
||||||
|
unfiltered_dataframe[f'constant_{i}'] = i
|
||||||
|
|
||||||
unfiltered_dataframe = freqai.dk.slice_dataframe(new_timerange, unfiltered_dataframe)
|
unfiltered_dataframe = freqai.dk.slice_dataframe(new_timerange, unfiltered_dataframe)
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
|
|||||||
("CatboostClassifier", 6, "freqai_test_classifier")
|
("CatboostClassifier", 6, "freqai_test_classifier")
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_start_backtesting(mocker, freqai_conf, model, num_files, strat):
|
def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog):
|
||||||
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
||||||
freqai_conf['runmode'] = RunMode.BACKTEST
|
freqai_conf['runmode'] = RunMode.BACKTEST
|
||||||
Trade.use_db = False
|
Trade.use_db = False
|
||||||
@ -181,12 +181,23 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat):
|
|||||||
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
||||||
|
|
||||||
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
|
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
|
||||||
|
for i in range(5):
|
||||||
|
df[f'%-constant_{i}'] = i
|
||||||
|
# df.loc[:, f'%-constant_{i}'] = i
|
||||||
|
|
||||||
metadata = {"pair": "LTC/BTC"}
|
metadata = {"pair": "LTC/BTC"}
|
||||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||||
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
||||||
|
|
||||||
assert len(model_folders) == num_files
|
assert len(model_folders) == num_files
|
||||||
|
assert log_has_re(
|
||||||
|
"Removed features ",
|
||||||
|
caplog,
|
||||||
|
)
|
||||||
|
assert log_has_re(
|
||||||
|
"Removed 5 features from prediction features, ",
|
||||||
|
caplog,
|
||||||
|
)
|
||||||
Backtesting.cleanup()
|
Backtesting.cleanup()
|
||||||
shutil.rmtree(Path(freqai.dk.full_path))
|
shutil.rmtree(Path(freqai.dk.full_path))
|
||||||
|
|
||||||
@ -256,6 +267,7 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
|||||||
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
||||||
|
|
||||||
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
|
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
|
||||||
|
|
||||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||||
|
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
@ -312,6 +324,7 @@ def test_follow_mode(mocker, freqai_conf):
|
|||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
|
||||||
df = strategy.dp.get_pair_dataframe('ADA/BTC', '5m')
|
df = strategy.dp.get_pair_dataframe('ADA/BTC', '5m')
|
||||||
|
|
||||||
freqai.start_live(df, metadata, strategy, freqai.dk)
|
freqai.start_live(df, metadata, strategy, freqai.dk)
|
||||||
|
|
||||||
assert len(freqai.dk.return_dataframe.index) == 5702
|
assert len(freqai.dk.return_dataframe.index) == 5702
|
||||||
|
@ -99,6 +99,7 @@ def test_send_msg_telegram_error(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
def test_process_msg_queue(mocker, default_conf, caplog) -> None:
|
def test_process_msg_queue(mocker, default_conf, caplog) -> None:
|
||||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg')
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg')
|
||||||
|
default_conf['telegram']['allow_custom_messages'] = True
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
@ -108,8 +109,8 @@ def test_process_msg_queue(mocker, default_conf, caplog) -> None:
|
|||||||
queue.append('Test message 2')
|
queue.append('Test message 2')
|
||||||
rpc_manager.process_msg_queue(queue)
|
rpc_manager.process_msg_queue(queue)
|
||||||
|
|
||||||
assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message'}", caplog)
|
assert log_has("Sending rpc strategy_msg: Test message", caplog)
|
||||||
assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message 2'}", caplog)
|
assert log_has("Sending rpc strategy_msg: Test message 2", caplog)
|
||||||
assert telegram_mock.call_count == 2
|
assert telegram_mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
from freqtrade.enums import ExitType, RPCMessageType
|
from freqtrade.enums import ExitType, RPCMessageType
|
||||||
@ -337,34 +336,18 @@ def test_exception_send_msg(default_conf, mocker, caplog):
|
|||||||
caplog)
|
caplog)
|
||||||
|
|
||||||
default_conf["webhook"] = get_webhook_dict()
|
default_conf["webhook"] = get_webhook_dict()
|
||||||
default_conf["webhook"]["webhookentry"]["value1"] = "{DEADBEEF:8f}"
|
default_conf["webhook"]["strategy_msg"] = {"value1": "{DEADBEEF:8f}"}
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||||
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
|
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.ENTRY,
|
'type': RPCMessageType.STRATEGY_MSG,
|
||||||
'exchange': 'Binance',
|
'msg': 'hello world',
|
||||||
'pair': 'ETH/BTC',
|
|
||||||
'limit': 0.005,
|
|
||||||
'order_type': 'limit',
|
|
||||||
'stake_amount': 0.8,
|
|
||||||
'stake_amount_fiat': 500,
|
|
||||||
'stake_currency': 'BTC',
|
|
||||||
'fiat_currency': 'EUR'
|
|
||||||
}
|
}
|
||||||
webhook.send_msg(msg)
|
webhook.send_msg(msg)
|
||||||
assert log_has("Problem calling Webhook. Please check your webhook configuration. "
|
assert log_has("Problem calling Webhook. Please check your webhook configuration. "
|
||||||
"Exception: 'DEADBEEF'", caplog)
|
"Exception: 'DEADBEEF'", caplog)
|
||||||
|
|
||||||
msg_mock = MagicMock()
|
|
||||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
|
||||||
msg = {
|
|
||||||
'type': 'DEADBEEF',
|
|
||||||
'status': 'whatever'
|
|
||||||
}
|
|
||||||
with pytest.raises(NotImplementedError):
|
|
||||||
webhook.send_msg(msg)
|
|
||||||
|
|
||||||
# Test no failure for not implemented but known messagetypes
|
# Test no failure for not implemented but known messagetypes
|
||||||
for e in RPCMessageType:
|
for e in RPCMessageType:
|
||||||
msg = {
|
msg = {
|
||||||
|
Loading…
Reference in New Issue
Block a user