Merge branch 'freqtrade:develop' into develop

This commit is contained in:
hippocritical 2023-04-30 10:20:40 +02:00 committed by GitHub
commit ce979b21f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 884 additions and 584 deletions

View File

@ -57,7 +57,7 @@ jobs:
- name: Installation - *nix
if: runner.os == 'Linux'
run: |
python -m pip install --upgrade pip==23.0.1 wheel
python -m pip install --upgrade pip==23.0.1 wheel==0.38.4
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
@ -163,7 +163,7 @@ jobs:
rm /usr/local/bin/python3.11-config || true
brew install hdf5 c-blosc
python -m pip install --upgrade pip==23.0.1 wheel
python -m pip install --upgrade pip==23.0.1 wheel==0.38.4
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
@ -352,7 +352,7 @@ jobs:
- name: Installation - *nix
if: runner.os == 'Linux'
run: |
python -m pip install --upgrade pip==23.0.1 wheel
python -m pip install --upgrade pip==23.0.1 wheel==0.38.4
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include

View File

@ -18,7 +18,7 @@ repos:
- types-requests==2.28.11.17
- types-tabulate==0.9.0.2
- types-python-dateutil==2.8.19.12
- SQLAlchemy==2.0.9
- SQLAlchemy==2.0.10
# stages: [push]
- repo: https://github.com/pycqa/isort

View File

@ -25,7 +25,7 @@ FROM base as python-deps
RUN apt-get update \
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
&& apt-get clean \
&& pip install --upgrade pip==23.0.1
&& pip install --upgrade pip==23.0.1 wheel==0.38.4
# Install TA-lib
COPY build_helpers/* /tmp/

View File

@ -1,7 +1,7 @@
# Downloads don't work automatically, since the URL is regenerated via javascript.
# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib
python -m pip install --upgrade pip==23.0.1 wheel
python -m pip install --upgrade pip==23.0.1 wheel==0.38.4
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"

View File

@ -138,7 +138,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account.[More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float.
| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float.
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
@ -155,25 +155,25 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates) <br>*Defaults to None.*<br> **Datatype:** Float
| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates) <br>*Defaults to `None`.*<br> **Datatype:** Float
| `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md). <br>*Defaults to `"spot"`.* <br> **Datatype:** String
| `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md). <br> **Datatype:** String
| `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md). <br>*Defaults to `0.05`.* <br> **Datatype:** Float
| | **Unfilled timeout**
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `"minutes"`.* <br> **Datatype:** String
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
| | **Pricing**
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#buy-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).<br> *Defaults to `"same"`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
| `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled).
| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled). <br> *Defaults to `true`.*<br> **Datatype:** Boolean
| `entry_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to enter a trade. I.e. a value of 2 will allow the bot to pick the 2nd entry in [Order Book Entry](#entry-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `entry_pricing. check_depth_of_market.enabled` | Do not enter if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `entry_pricing. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).<br> *Defaults to `"same"`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
| `exit_pricing.price_last_balance` | Interpolate the exiting price. More information [below](#exit-price-without-orderbook-enabled).
| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled). <br> *Defaults to `true`.*<br> **Datatype:** Boolean
| `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
| | **TODO**
@ -199,10 +199,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`<br> **Datatype:** Boolean
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| | **Plugins**
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
@ -213,7 +213,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `True`.<br> **Datatype:** boolean
| `telegram.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.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function. <br> **Datatype:** Boolean
| | **Webhook**

View File

@ -142,6 +142,13 @@ To fix this, redefine order types in the strategy to use "limit" instead of "mar
The same fix should be applied in the configuration file, if order types are defined in your custom config rather than in the strategy.
### I'm trying to start the bot live, but get an API permission error
Errors like `Invalid API-key, IP, or permissions for action` mean exactly what they actually say.
Your API key is either invalid (copy/paste error? check for leading/trailing spaces in the config), expired, or the IP you're running the bot from is not enabled in the Exchange's API console.
Usually, the permission "Spot Trading" (or the equivalent in the exchange you use) will be necessary.
Futures will usually have to be enabled specifically.
### How do I search the bot logs for something?
By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility sub-commands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout.

View File

@ -52,7 +52,7 @@ The FreqAI strategy requires including the following lines of code in the standa
return dataframe
def feature_engineering_expand_all(self, dataframe, period, **kwargs):
def feature_engineering_expand_all(self, dataframe: DataFrame, period, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
@ -77,7 +77,7 @@ The FreqAI strategy requires including the following lines of code in the standa
return dataframe
def feature_engineering_expand_basic(self, dataframe, **kwargs):
def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
@ -101,7 +101,7 @@ The FreqAI strategy requires including the following lines of code in the standa
dataframe["%-raw_price"] = dataframe["close"]
return dataframe
def feature_engineering_standard(self, dataframe, **kwargs):
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
@ -122,7 +122,7 @@ The FreqAI strategy requires including the following lines of code in the standa
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
return dataframe
def set_freqai_targets(self, dataframe, **kwargs):
def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.
@ -139,6 +139,7 @@ The FreqAI strategy requires including the following lines of code in the standa
/ dataframe["close"]
- 1
)
return dataframe
```
Notice how the `feature_engineering_*()` is where [features](freqai-feature-engineering.md#feature-engineering) are added. Meanwhile `set_freqai_targets()` adds the labels/targets. A full example strategy is available in `templates/FreqaiExampleStrategy.py`.
@ -386,7 +387,7 @@ Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. T
For example, if you are using a binary classifier to predict price movements as up or down, you can set the class names as follows:
```python
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
self.freqai.class_names = ["down", "up"]
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) >
dataframe["close"], 'up', 'down')

View File

@ -16,7 +16,7 @@ Meanwhile, high level feature engineering is handled within `"feature_parameters
It is advisable to start from the template `feature_engineering_*` functions in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy:
```python
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
@ -67,7 +67,7 @@ It is advisable to start from the template `feature_engineering_*` functions in
return dataframe
def feature_engineering_expand_basic(self, dataframe, metadata, **kwargs):
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
@ -96,7 +96,7 @@ It is advisable to start from the template `feature_engineering_*` functions in
dataframe["%-raw_price"] = dataframe["close"]
return dataframe
def feature_engineering_standard(self, dataframe, metadata, **kwargs):
def feature_engineering_standard(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
@ -122,7 +122,7 @@ It is advisable to start from the template `feature_engineering_*` functions in
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
return dataframe
def set_freqai_targets(self, dataframe, metadata, **kwargs):
def set_freqai_targets(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.
@ -181,15 +181,14 @@ You can ask for each of the defined features to be included also for informative
In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
$= 3 * 3 * 3 * 2 * 2 = 108$.
### Gain finer control over `feature_engineering_*` functions with `metadata`
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
```python
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
if metadata["tf"] == "1h":
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
```python
def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame:
if metadata["tf"] == "1h":
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
```
This will block `ta.ROC()` from being added to any timeframes other than `"1h"`.

View File

@ -37,7 +37,7 @@ freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --con
where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesn't require them. However, FreqAI requires a default (neutral) value to be set in the action column:
```python
def set_freqai_targets(self, dataframe, **kwargs):
def set_freqai_targets(self, dataframe, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.
@ -53,17 +53,19 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from
# For RL, there are no direct targets to set. This is filler (neutral)
# until the agent sends an action.
dataframe["&-action"] = 0
return dataframe
```
Most of the function remains the same as for typical Regressors, however, the function below shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
```python
def feature_engineering_standard(self, dataframe, **kwargs):
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
# The following features are necessary for RL models
dataframe[f"%-raw_close"] = dataframe["close"]
dataframe[f"%-raw_open"] = dataframe["open"]
dataframe[f"%-raw_high"] = dataframe["high"]
dataframe[f"%-raw_low"] = dataframe["low"]
return dataframe
```
Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action.

View File

@ -49,7 +49,7 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect
| `wait_timeout` | Timeout until we ping again if no message is received. <br>*Defaults to `300`.*<br> **Datatype:** Integer - in seconds.
| `ping_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
| `sleep_time` | Sleep time before retrying to connect.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `False`.*<br> **Datatype:** Boolean.
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `false`.*<br> **Datatype:** Boolean.
| `message_size_limit` | Size limit per message<br>*Defaults to `8`.*<br> **Datatype:** Integer - Megabytes.
Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist.

View File

@ -1,6 +1,6 @@
markdown==3.3.7
mkdocs==1.4.2
mkdocs-material==9.1.6
mkdocs-material==9.1.7
mdx_truly_sane_lists==1.3
pymdown-extensions==9.11
jinja2==3.1.2

View File

@ -209,11 +209,6 @@ You can also keep a static stoploss until the offset is reached, and then trail
If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`.
This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset.
``` python
trailing_stop_positive_offset = 0.011
trailing_only_offset_is_reached = True
```
Configuration (offset is buy-price + 3%):
``` python

View File

@ -1,21 +1,21 @@
# Advanced Strategies
This page explains some advanced concepts available for strategies.
If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation and with the [Freqtrade basics](bot-basics.md) first.
If you're just getting started, please familiarize yourself with the [Freqtrade basics](bot-basics.md) and methods described in [Strategy Customization](strategy-customization.md) first.
[Freqtrade basics](bot-basics.md) describes in which sequence each method described below is called, which can be helpful to understand which method to use for your custom needs.
The call sequence of the methods described here is covered under [bot execution logic](bot-basics.md#bot-execution-logic). Those docs are also helpful in deciding which method is most suitable for your customisation needs.
!!! Note
All callback methods described below should only be implemented in a strategy if they are actually used.
Callback methods should *only* be implemented if a strategy uses them.
!!! Tip
You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced`
Start off with a strategy template containing all available callback methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced`
## Storing information
Storing information can be accomplished by creating a new dictionary within the strategy class.
The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables.
```python
class AwesomeStrategy(IStrategy):

View File

@ -43,7 +43,7 @@ class AwesomeStrategy(IStrategy):
if self.config['runmode'].value in ('live', 'dry_run'):
# Assign this to the class by using self.*
# can then be used by populate_* methods
self.cust_remote_data = requests.get('https://some_remote_source.example.com')
self.custom_remote_data = requests.get('https://some_remote_source.example.com')
```
@ -352,7 +352,7 @@ class AwesomeStrategy(IStrategy):
# Convert absolute price to percentage relative to current_rate
if stoploss_price < current_rate:
return (stoploss_price / current_rate) - 1
return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short)
# return maximum stoploss value, keeping current stoploss price unchanged
return 1

View File

@ -578,7 +578,7 @@ def populate_any_indicators(
Features will now expand automatically. As such, the expansion loops, as well as the `{pair}` / `{timeframe}` parts will need to be removed.
``` python linenums="1"
def feature_engineering_expand_all(self, dataframe, period, **kwargs):
def feature_engineering_expand_all(self, dataframe, period, **kwargs) -> DataFrame::
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
@ -638,7 +638,7 @@ Features will now expand automatically. As such, the expansion loops, as well as
Basic features. Make sure to remove the `{pair}` part from your features.
``` python linenums="1"
def feature_engineering_expand_basic(self, dataframe, **kwargs):
def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame::
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
@ -673,7 +673,7 @@ Basic features. Make sure to remove the `{pair}` part from your features.
### FreqAI - feature engineering standard
``` python linenums="1"
def feature_engineering_standard(self, dataframe, **kwargs):
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
@ -704,7 +704,7 @@ Basic features. Make sure to remove the `{pair}` part from your features.
Targets now get their own, dedicated method.
``` python linenums="1"
def set_freqai_targets(self, dataframe, **kwargs):
def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.

View File

@ -191,7 +191,8 @@ official commands. You can ask at any moment for help with `/help`.
| **Metrics** |
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
| `/performance` | Show performance of each finished trade grouped by pair
| `/balance` | Show account balance per currency
| `/balance` | Show bot managed balance per currency
| `/balance full` | Show account balance per currency
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
| `/weekly <n>` | Shows profit or loss per week, over the last n weeks (n defaults to 8)
| `/monthly <n>` | Shows profit or loss per month, over the last n months (n defaults to 6)
@ -202,7 +203,6 @@ official commands. You can ask at any moment for help with `/help`.
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
| `/edge` | Show validated pairs by Edge if it is enabled.
## Telegram commands in action
Below, example of Telegram message you will receive for each command.

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = '2023.4.dev'
__version__ = '2023.5.dev'
if 'dev' in __version__:
from pathlib import Path

View File

@ -46,7 +46,7 @@ ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_coloriz
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list"]
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"]
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]

View File

@ -107,8 +107,7 @@ class Exchange:
# Lock event loop. This is necessary to avoid race-conditions when using force* commands
# Due to funding fee fetching.
self._loop_lock = Lock()
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.loop = self._init_async_loop()
self._config: Config = {}
self._config.update(config)
@ -212,6 +211,11 @@ class Exchange:
if self.loop and not self.loop.is_closed():
self.loop.close()
def _init_async_loop(self) -> asyncio.AbstractEventLoop:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop
def validate_config(self, config):
# Check if timeframe is available
self.validate_timeframes(config.get('timeframe'))
@ -863,7 +867,7 @@ class Exchange:
}
if stop_loss:
dry_order["info"] = {"stopPrice": dry_order["price"]}
dry_order["stopPrice"] = dry_order["price"]
dry_order[self._ft_has['stop_price_param']] = dry_order["price"]
# Workaround to avoid filling stoploss orders immediately
dry_order["ft_order_type"] = "stoploss"
orderbook: Optional[OrderBook] = None
@ -1015,7 +1019,7 @@ class Exchange:
from freqtrade.persistence import Order
order = Order.order_by_id(order_id)
if order:
ccxt_order = order.to_ccxt_object()
ccxt_order = order.to_ccxt_object(self._ft_has['stop_price_param'])
self._dry_run_open_orders[order_id] = ccxt_order
return ccxt_order
# Gracefully handle errors with dry-run orders.
@ -2431,14 +2435,17 @@ class Exchange:
def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]:
filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
if filename.is_file():
tiers = file_load_json(filename)
updated = tiers.get('updated')
if updated:
updated_dt = parser.parse(updated)
if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4):
logger.info("Cached leverage tiers are outdated. Will update.")
return None
return tiers['data']
try:
tiers = file_load_json(filename)
updated = tiers.get('updated')
if updated:
updated_dt = parser.parse(updated)
if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4):
logger.info("Cached leverage tiers are outdated. Will update.")
return None
return tiers['data']
except Exception:
logger.exception("Error loading cached leverage tiers. Refreshing.")
return None
def fill_leverage_tiers(self) -> None:

View File

@ -489,9 +489,9 @@ class IFreqaiModel(ABC):
if dk.training_features_list != feature_list:
raise OperationalException(
"Trying to access pretrained model with `identifier` "
"but found different features furnished by current strategy."
"Change `identifier` to train from scratch, or ensure the"
"strategy is furnishing the same features as the pretrained"
"but found different features furnished by current strategy. "
"Change `identifier` to train from scratch, or ensure the "
"strategy is furnishing the same features as the pretrained "
"model. In case of --strategy-list, please be aware that FreqAI "
"requires all strategies to maintain identical "
"feature_engineering_* functions"

View File

@ -420,7 +420,7 @@ class FreqtradeBot(LoggingMixin):
"""
Try refinding a lost trade.
Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy).
Tries to walk the stored orders and sell them off eventually.
Tries to walk the stored orders and updates the trade state if necessary.
"""
logger.info(f"Trying to refind lost order for {trade}")
for order in trade.orders:
@ -490,7 +490,8 @@ class FreqtradeBot(LoggingMixin):
# Create entity and execute trade for each pair from whitelist
for pair in whitelist:
try:
trades_created += self.create_trade(pair)
with self._exit_lock:
trades_created += self.create_trade(pair)
except DependencyException as exception:
logger.warning('Unable to create trade for %s: %s', pair, exception)
@ -1425,7 +1426,7 @@ class FreqtradeBot(LoggingMixin):
corder = order
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('%s order %s for %s.', side, reason, trade)
logger.info(f'{side} order {reason} for {trade}.')
# Using filled to determine the filled amount
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
@ -1507,7 +1508,7 @@ class FreqtradeBot(LoggingMixin):
trade.exit_reason = None
trade.open_order_id = None
self.update_trade_state(trade, trade.open_order_id, order)
self.update_trade_state(trade, order['id'], order)
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
trade.close_rate = None

View File

@ -379,7 +379,8 @@ class Hyperopt:
strat_stats = generate_strategy_stats(
self.pairlist, self.backtesting.strategy.get_strategy_name(),
backtesting_results, min_date, max_date, market_change=self.market_change
backtesting_results, min_date, max_date, market_change=self.market_change,
is_hyperopt=True,
)
results_explanation = HyperoptTools.format_results_explanation_string(
strat_stats, self.config['stake_currency'])

View File

@ -7,8 +7,8 @@ from typing import Any, Dict, List, Union
from pandas import DataFrame, to_datetime
from tabulate import tabulate
from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT,
Config, IntOrInf)
from freqtrade.constants import (BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN,
UNLIMITED_STAKE_AMOUNT, Config, IntOrInf)
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
calculate_expectancy, calculate_market_change,
calculate_max_drawdown, calculate_sharpe, calculate_sortino)
@ -296,6 +296,7 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic
stats.append(
{
'date': name.strftime('%d/%m/%Y'),
'date_ts': int(name.to_pydatetime().timestamp() * 1000),
'profit_abs': profit_abs,
'wins': wins,
'draws': draws,
@ -305,6 +306,13 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic
return stats
def generate_all_periodic_breakdown_stats(trade_list: List) -> Dict[str, List]:
result = {}
for period in BACKTEST_BREAKDOWNS:
result[period] = generate_periodic_breakdown_stats(trade_list, period)
return result
def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
""" Generate overall trade statistics """
if len(results) == 0:
@ -381,7 +389,8 @@ def generate_strategy_stats(pairlist: List[str],
strategy: str,
content: Dict[str, Any],
min_date: datetime, max_date: datetime,
market_change: float
market_change: float,
is_hyperopt: bool = False,
) -> Dict[str, Any]:
"""
:param pairlist: List of pairs to backtest
@ -416,6 +425,11 @@ def generate_strategy_stats(pairlist: List[str],
daily_stats = generate_daily_stats(results)
trade_stats = generate_trading_stats(results)
periodic_breakdown = {}
if not is_hyperopt:
periodic_breakdown = {'periodic_breakdown': generate_all_periodic_breakdown_stats(results)}
best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
@ -434,7 +448,6 @@ def generate_strategy_stats(pairlist: List[str],
'results_per_enter_tag': enter_tag_results,
'exit_reason_summary': exit_reason_stats,
'left_open_trades': left_open_results,
# 'days_breakdown_stats': days_breakdown_stats,
'total_trades': len(results),
'trade_count_long': len(results.loc[~results['is_short']]),
@ -499,6 +512,7 @@ def generate_strategy_stats(pairlist: List[str],
'exit_profit_only': config['exit_profit_only'],
'exit_profit_offset': config['exit_profit_offset'],
'ignore_roi_if_entry_signal': config['ignore_roi_if_entry_signal'],
**periodic_breakdown,
**daily_stats,
**trade_stats
}
@ -891,8 +905,11 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
print(table)
for period in backtest_breakdown:
days_breakdown_stats = generate_periodic_breakdown_stats(
trade_list=results['trades'], period=period)
if period in results.get('periodic_breakdown', {}):
days_breakdown_stats = results['periodic_breakdown'][period]
else:
days_breakdown_stats = generate_periodic_breakdown_stats(
trade_list=results['trades'], period=period)
table = text_table_periodic_breakdown(days_breakdown_stats=days_breakdown_stats,
stake_currency=stake_currency, period=period)
if isinstance(table, str) and len(table) > 0:

View File

@ -36,7 +36,7 @@ class _KeyValueStoreModel(ModelBase):
value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False)
string_value: Mapped[Optional[str]]
string_value: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
datetime_value: Mapped[Optional[datetime]]
float_value: Mapped[Optional[float]]
int_value: Mapped[Optional[int]]

View File

@ -158,7 +158,7 @@ class Order(ModelBase):
self.order_filled_date = datetime.now(timezone.utc)
self.order_update_date = datetime.now(timezone.utc)
def to_ccxt_object(self) -> Dict[str, Any]:
def to_ccxt_object(self, stopPriceName: str = 'stopPrice') -> Dict[str, Any]:
order: Dict[str, Any] = {
'id': self.order_id,
'symbol': self.ft_pair,
@ -170,7 +170,6 @@ class Order(ModelBase):
'side': self.ft_order_side,
'filled': self.filled,
'remaining': self.remaining,
'stopPrice': self.stop_price,
'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'),
'timestamp': int(self.order_date_utc.timestamp() * 1000),
'status': self.status,
@ -178,7 +177,11 @@ class Order(ModelBase):
'info': {},
}
if self.ft_order_side == 'stoploss':
order['ft_order_type'] = 'stoploss'
order.update({
stopPriceName: self.stop_price,
'ft_order_type': 'stoploss',
})
return order
def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
@ -708,7 +711,10 @@ class LocalTrade():
if order.ft_order_side != self.entry_side:
amount_tr = amount_to_contract_precision(self.amount, self.amount_precision,
self.precision_mode, self.contract_size)
if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC):
if (
isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC)
or order.safe_amount_after_fee > amount_tr
):
self.close(order.safe_price)
else:
self.recalc_trade_from_orders()

View File

@ -36,20 +36,25 @@ class Balance(BaseModel):
free: float
balance: float
used: float
bot_owned: Optional[float]
est_stake: float
est_stake_bot: Optional[float]
stake: str
# Starting with 2.x
side: str
leverage: float
is_position: bool
position: float
is_bot_managed: bool
class Balances(BaseModel):
currencies: List[Balance]
total: float
total_bot: float
symbol: str
value: float
value_bot: float
stake: str
note: str
starting_capital: float

View File

@ -43,7 +43,8 @@ logger = logging.getLogger(__name__)
# 2.23: Allow plot config request in webserver mode
# 2.24: Add cancel_open_order endpoint
# 2.25: Add several profit values to /status endpoint
API_VERSION = 2.25
# 2.26: increase /balance output
API_VERSION = 2.26
# Public API, requires no auth.
router_public = APIRouter()
@ -303,11 +304,11 @@ def get_strategy(strategy: str, config=Depends(get_config)):
@router.get('/freqaimodels', response_model=FreqAIModelListResponse, tags=['freqai'])
def list_freqaimodels(config=Depends(get_config)):
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
strategies = FreqaiModelResolver.search_all_objects(
models = FreqaiModelResolver.search_all_objects(
config, False)
strategies = sorted(strategies, key=lambda x: x['name'])
models = sorted(models, key=lambda x: x['name'])
return {'freqaimodels': [x['name'] for x in strategies]}
return {'freqaimodels': [x['name'] for x in models]}
@router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data'])

View File

@ -24,6 +24,7 @@ from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirecti
State, TradingMode)
from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.exchange.types import Tickers
from freqtrade.loggers import bufferHandler
from freqtrade.misc import decimals_per_coin, shorten_date
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, PairLocks, Trade
@ -581,15 +582,44 @@ class RPC:
'bot_start_date': bot_start.strftime(DATETIME_PRINT_FORMAT) if bot_start else '',
}
def __balance_get_est_stake(
self, coin: str, stake_currency: str, amount: float,
balance: Wallet, tickers) -> Tuple[float, float]:
est_stake = 0.0
est_bot_stake = 0.0
if coin == stake_currency:
est_stake = balance.total
if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
# in Futures, "total" includes the locked stake, and therefore all positions
est_stake = balance.free
est_bot_stake = amount
else:
try:
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
rate: Optional[float] = tickers.get(pair, {}).get('last', None)
if rate:
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
rate = 1.0 / rate
est_stake = rate * balance.total
est_bot_stake = rate * amount
except (ExchangeError):
logger.warning(f"Could not get rate for pair {coin}.")
raise ValueError()
return est_stake, est_bot_stake
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
""" Returns current account balance per crypto """
currencies: List[Dict] = []
total = 0.0
total_bot = 0.0
try:
tickers = self._freqtrade.exchange.get_tickers(cached=True)
tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True)
except (ExchangeError):
raise RPCException('Error getting current tickers.')
open_trades: List[Trade] = Trade.get_open_trades()
open_assets: Dict[str, Trade] = {t.safe_base_currency: t for t in open_trades}
self._freqtrade.wallets.update(require_update=False)
starting_capital = self._freqtrade.wallets.get_starting_balance()
starting_cap_fiat = self._fiat_converter.convert_amount(
@ -600,41 +630,42 @@ class RPC:
if not balance.total:
continue
est_stake: float = 0
trade = open_assets.get(coin, None)
is_bot_managed = coin == stake_currency or trade is not None
trade_amount = trade.amount if trade else 0
if coin == stake_currency:
rate = 1.0
est_stake = balance.total
if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
# in Futures, "total" includes the locked stake, and therefore all positions
est_stake = balance.free
else:
try:
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
rate = tickers.get(pair, {}).get('last')
if rate:
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
rate = 1.0 / rate
est_stake = rate * balance.total
except (ExchangeError):
logger.warning(f" Could not get rate for pair {coin}.")
continue
total = total + est_stake
trade_amount = self._freqtrade.wallets.get_available_stake_amount()
try:
est_stake, est_stake_bot = self.__balance_get_est_stake(
coin, stake_currency, trade_amount, balance, tickers)
except ValueError:
continue
total += est_stake
if is_bot_managed:
total_bot += est_stake_bot
currencies.append({
'currency': coin,
'free': balance.free,
'balance': balance.total,
'used': balance.used,
'bot_owned': trade_amount,
'est_stake': est_stake or 0,
'est_stake_bot': est_stake_bot if is_bot_managed else 0,
'stake': stake_currency,
'side': 'long',
'leverage': 1,
'position': 0,
'is_bot_managed': is_bot_managed,
'is_position': False,
})
symbol: str
position: PositionWallet
for symbol, position in self._freqtrade.wallets.get_all_positions().items():
total += position.collateral
total_bot += position.collateral
currencies.append({
'currency': symbol,
@ -643,24 +674,30 @@ class RPC:
'used': 0,
'position': position.position,
'est_stake': position.collateral,
'est_stake_bot': position.collateral,
'stake': stake_currency,
'leverage': position.leverage,
'side': position.side,
'is_bot_managed': True,
'is_position': True
})
value = self._fiat_converter.convert_amount(
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
value_bot = self._fiat_converter.convert_amount(
total_bot, stake_currency, fiat_display_currency) if self._fiat_converter else 0
trade_count = len(Trade.get_trades_proxy())
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
starting_capital_ratio = (total_bot / starting_capital) - 1 if starting_capital else 0.0
starting_cap_fiat_ratio = (value_bot / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
return {
'currencies': currencies,
'total': total,
'total_bot': total_bot,
'symbol': fiat_display_currency,
'value': value,
'value_bot': value_bot,
'stake': stake_currency,
'starting_capital': starting_capital,
'starting_capital_ratio': starting_capital_ratio,

View File

@ -3,6 +3,7 @@
"""
This module manage Telegram communication
"""
import asyncio
import json
import logging
import re
@ -13,15 +14,17 @@ from functools import partial
from html import escape
from itertools import chain
from math import isnan
from typing import Any, Callable, Dict, List, Optional, Union
from threading import Thread
from typing import Any, Callable, Coroutine, Dict, List, Optional, Union
import arrow
from tabulate import tabulate
from telegram import (MAX_MESSAGE_LENGTH, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup,
KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update)
from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton,
ReplyKeyboardMarkup, Update)
from telegram.constants import MessageLimit, ParseMode
from telegram.error import BadRequest, NetworkError, TelegramError
from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater
from telegram.utils.helpers import escape_markdown
from telegram.ext import Application, CallbackContext, CallbackQueryHandler, CommandHandler
from telegram.helpers import escape_markdown
from freqtrade.__init__ import __version__
from freqtrade.constants import DUST_PER_COIN, Config
@ -33,6 +36,9 @@ from freqtrade.rpc import RPC, RPCException, RPCHandler
from freqtrade.rpc.rpc_types import RPCSendMsg
MAX_MESSAGE_LENGTH = MessageLimit.MAX_TEXT_LENGTH
logger = logging.getLogger(__name__)
logger.debug('Included module rpc.telegram ...')
@ -47,14 +53,14 @@ class TimeunitMappings:
default: int
def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]):
"""
Decorator to check if the message comes from the correct chat_id
:param command_handler: Telegram CommandHandler
:return: decorated function
"""
def wrapper(self, *args, **kwargs):
async def wrapper(self, *args, **kwargs):
""" Decorator logic """
update = kwargs.get('update') or args[0]
@ -76,9 +82,9 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
chat_id
)
try:
return command_handler(self, *args, **kwargs)
return await command_handler(self, *args, **kwargs)
except RPCException as e:
self._send_msg(str(e))
await self._send_msg(str(e))
except BaseException:
logger.exception('Exception occurred within Telegram module')
finally:
@ -99,9 +105,17 @@ class Telegram(RPCHandler):
"""
super().__init__(rpc, config)
self._updater: Updater
self._app: Application
self._loop: asyncio.AbstractEventLoop
self._init_keyboard()
self._init()
self._start_thread()
def _start_thread(self):
"""
Creates and starts the polling thread
"""
self._thread = Thread(target=self._init, name='FTTelegram')
self._thread.start()
def _init_keyboard(self) -> None:
"""
@ -152,14 +166,23 @@ class Telegram(RPCHandler):
logger.info('using custom keyboard from '
f'config.json: {self._keyboard}')
def _init_telegram_app(self):
return Application.builder().token(self._config['telegram']['token']).build()
def _init(self) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
Runs in a separate thread.
"""
self._updater = Updater(token=self._config['telegram']['token'], workers=0,
use_context=True)
try:
self._loop = asyncio.get_running_loop()
except RuntimeError:
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
self._app = self._init_telegram_app()
# Register command handler and start telegram message polling
handles = [
@ -218,21 +241,38 @@ class Telegram(RPCHandler):
CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"),
]
for handle in handles:
self._updater.dispatcher.add_handler(handle)
self._app.add_handler(handle)
for callback in callbacks:
self._updater.dispatcher.add_handler(callback)
self._app.add_handler(callback)
self._updater.start_polling(
bootstrap_retries=-1,
timeout=20,
read_latency=60, # Assumed transmission latency
drop_pending_updates=True,
)
logger.info(
'rpc.telegram is listening for following commands: %s',
[h.command for h in handles]
[[x for x in sorted(h.commands)] for h in handles]
)
self._loop.run_until_complete(self._startup_telegram())
async def _startup_telegram(self) -> None:
await self._app.initialize()
await self._app.start()
if self._app.updater:
await self._app.updater.start_polling(
bootstrap_retries=-1,
timeout=20,
# read_latency=60, # Assumed transmission latency
drop_pending_updates=True,
# stop_signals=[], # Necessary as we don't run on the main thread
)
while True:
await asyncio.sleep(10)
if not self._app.updater.running:
break
async def _cleanup_telegram(self) -> None:
if self._app.updater:
await self._app.updater.stop()
await self._app.stop()
await self._app.shutdown()
def cleanup(self) -> None:
"""
@ -240,7 +280,8 @@ class Telegram(RPCHandler):
:return: None
"""
# This can take up to `timeout` from the call to `start_polling`.
self._updater.stop()
asyncio.run_coroutine_threadsafe(self._cleanup_telegram(), self._loop)
self._thread.join()
def _exchange_from_msg(self, msg: Dict[str, Any]) -> str:
"""
@ -453,7 +494,9 @@ class Telegram(RPCHandler):
message = self.compose_message(deepcopy(msg), msg_type) # type: ignore
if message:
self._send_msg(message, disable_notification=(noti == 'silent'))
asyncio.run_coroutine_threadsafe(
self._send_msg(message, disable_notification=(noti == 'silent')),
self._loop)
def _get_sell_emoji(self, msg):
"""
@ -536,7 +579,7 @@ class Telegram(RPCHandler):
return lines_detail
@authorized_only
def _status(self, update: Update, context: CallbackContext) -> None:
async def _status(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /status.
Returns the current TradeThread status
@ -546,12 +589,12 @@ class Telegram(RPCHandler):
"""
if context.args and 'table' in context.args:
self._status_table(update, context)
await self._status_table(update, context)
return
else:
self._status_msg(update, context)
await self._status_msg(update, context)
def _status_msg(self, update: Update, context: CallbackContext) -> None:
async def _status_msg(self, update: Update, context: CallbackContext) -> None:
"""
handler for `/status` and `/status <id>`.
@ -635,9 +678,9 @@ class Telegram(RPCHandler):
lines_detail = self._prepare_order_details(
r['orders'], r['quote_currency'], r['is_open'])
lines.extend(lines_detail if lines_detail else "")
self.__send_status_msg(lines, r)
await self.__send_status_msg(lines, r)
def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
async def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
"""
Send status message.
"""
@ -648,13 +691,13 @@ class Telegram(RPCHandler):
if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
msg += line + '\n'
else:
self._send_msg(msg.format(**r))
await self._send_msg(msg.format(**r))
msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n'
self._send_msg(msg.format(**r))
await self._send_msg(msg.format(**r))
@authorized_only
def _status_table(self, update: Update, context: CallbackContext) -> None:
async def _status_table(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /status table.
Returns the current TradeThread status in table format
@ -687,12 +730,11 @@ class Telegram(RPCHandler):
# insert separators line between Total
lines = message.split("\n")
message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]])
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_status_table",
query=update.callback_query)
await self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_status_table",
query=update.callback_query)
@authorized_only
def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None:
async def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None:
"""
Handler for /daily <n>
Returns a daily profit (in BTC) over the last n days.
@ -739,11 +781,11 @@ class Telegram(RPCHandler):
f'<b>{val.message} Profit over the last {timescale} {val.message2}</b>:\n'
f'<pre>{stats_tab}</pre>'
)
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path=val.callback, query=update.callback_query)
await self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path=val.callback, query=update.callback_query)
@authorized_only
def _daily(self, update: Update, context: CallbackContext) -> None:
async def _daily(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /daily <n>
Returns a daily profit (in BTC) over the last n days.
@ -751,10 +793,10 @@ class Telegram(RPCHandler):
:param update: message update
:return: None
"""
self._timeunit_stats(update, context, 'days')
await self._timeunit_stats(update, context, 'days')
@authorized_only
def _weekly(self, update: Update, context: CallbackContext) -> None:
async def _weekly(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /weekly <n>
Returns a weekly profit (in BTC) over the last n weeks.
@ -762,10 +804,10 @@ class Telegram(RPCHandler):
:param update: message update
:return: None
"""
self._timeunit_stats(update, context, 'weeks')
await self._timeunit_stats(update, context, 'weeks')
@authorized_only
def _monthly(self, update: Update, context: CallbackContext) -> None:
async def _monthly(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /monthly <n>
Returns a monthly profit (in BTC) over the last n months.
@ -773,10 +815,10 @@ class Telegram(RPCHandler):
:param update: message update
:return: None
"""
self._timeunit_stats(update, context, 'months')
await self._timeunit_stats(update, context, 'months')
@authorized_only
def _profit(self, update: Update, context: CallbackContext) -> None:
async def _profit(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /profit.
Returns a cumulative profit statistics.
@ -850,11 +892,11 @@ class Telegram(RPCHandler):
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`"
)
self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
query=update.callback_query)
await self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
query=update.callback_query)
@authorized_only
def _stats(self, update: Update, context: CallbackContext) -> None:
async def _stats(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /stats
Show stats of recent trades
@ -885,7 +927,7 @@ class Telegram(RPCHandler):
headers=['Exit Reason', 'Exits', 'Wins', 'Losses']
)
if len(exit_reasons_tabulate) > 25:
self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN)
await self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN)
exit_reasons_msg = ''
durations = stats['durations']
@ -900,11 +942,12 @@ class Telegram(RPCHandler):
)
msg = (f"""```\n{exit_reasons_msg}```\n```\n{duration_msg}```""")
self._send_msg(msg, ParseMode.MARKDOWN)
await self._send_msg(msg, ParseMode.MARKDOWN)
@authorized_only
def _balance(self, update: Update, context: CallbackContext) -> None:
async def _balance(self, update: Update, context: CallbackContext) -> None:
""" Handler for /balance """
full_result = context.args and 'full' in context.args
result = self._rpc._rpc_balance(self._config['stake_currency'],
self._config.get('fiat_display_currency', ''))
@ -915,8 +958,7 @@ class Telegram(RPCHandler):
output = ''
if self._config['dry_run']:
output += "*Warning:* Simulated balances in Dry Mode.\n"
starting_cap = round_coin_value(
result['starting_capital'], self._config['stake_currency'])
starting_cap = round_coin_value(result['starting_capital'], self._config['stake_currency'])
output += f"Starting capital: `{starting_cap}`"
starting_cap_fiat = round_coin_value(
result['starting_capital_fiat'], self._config['fiat_display_currency']
@ -928,7 +970,10 @@ class Telegram(RPCHandler):
total_dust_currencies = 0
for curr in result['currencies']:
curr_output = ''
if curr['est_stake'] > balance_dust_level:
if (
(curr['is_position'] or curr['est_stake'] > balance_dust_level)
and (full_result or curr['is_bot_managed'])
):
if curr['is_position']:
curr_output = (
f"*{curr['currency']}:*\n"
@ -937,20 +982,24 @@ class Telegram(RPCHandler):
f"\t`Est. {curr['stake']}: "
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
else:
est_stake = round_coin_value(
curr['est_stake' if full_result else 'est_stake_bot'], curr['stake'], False)
curr_output = (
f"*{curr['currency']}:*\n"
f"\t`Available: {curr['free']:.8f}`\n"
f"\t`Balance: {curr['balance']:.8f}`\n"
f"\t`Pending: {curr['used']:.8f}`\n"
f"\t`Est. {curr['stake']}: "
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
f"\t`Bot Owned: {curr['bot_owned']:.8f}`\n"
f"\t`Est. {curr['stake']}: {est_stake}`\n")
elif curr['est_stake'] <= balance_dust_level:
total_dust_balance += curr['est_stake']
total_dust_currencies += 1
# Handle overflowing message length
if len(output + curr_output) >= MAX_MESSAGE_LENGTH:
self._send_msg(output)
await self._send_msg(output)
output = curr_output
else:
output += curr_output
@ -965,19 +1014,20 @@ class Telegram(RPCHandler):
tc = result['trade_count'] > 0
stake_improve = f" `({result['starting_capital_ratio']:.2%})`" if tc else ''
fiat_val = f" `({result['starting_capital_fiat_ratio']:.2%})`" if tc else ''
output += ("\n*Estimated Value*:\n"
f"\t`{result['stake']}: "
f"{round_coin_value(result['total'], result['stake'], False)}`"
f"{stake_improve}\n"
f"\t`{result['symbol']}: "
f"{round_coin_value(result['value'], result['symbol'], False)}`"
f"{fiat_val}\n")
self._send_msg(output, reload_able=True, callback_path="update_balance",
query=update.callback_query)
value = round_coin_value(
result['value' if full_result else 'value_bot'], result['symbol'], False)
total_stake = round_coin_value(
result['total' if full_result else 'total_bot'], result['stake'], False)
output += (
f"\n*Estimated Value{' (Bot managed assets only)' if not full_result else ''}*:\n"
f"\t`{result['stake']}: {total_stake}`{stake_improve}\n"
f"\t`{result['symbol']}: {value}`{fiat_val}\n"
)
await self._send_msg(output, reload_able=True, callback_path="update_balance",
query=update.callback_query)
@authorized_only
def _start(self, update: Update, context: CallbackContext) -> None:
async def _start(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /start.
Starts TradeThread
@ -986,10 +1036,10 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_start()
self._send_msg(f"Status: `{msg['status']}`")
await self._send_msg(f"Status: `{msg['status']}`")
@authorized_only
def _stop(self, update: Update, context: CallbackContext) -> None:
async def _stop(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /stop.
Stops TradeThread
@ -998,10 +1048,10 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_stop()
self._send_msg(f"Status: `{msg['status']}`")
await self._send_msg(f"Status: `{msg['status']}`")
@authorized_only
def _reload_config(self, update: Update, context: CallbackContext) -> None:
async def _reload_config(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /reload_config.
Triggers a config file reload
@ -1010,10 +1060,10 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_reload_config()
self._send_msg(f"Status: `{msg['status']}`")
await self._send_msg(f"Status: `{msg['status']}`")
@authorized_only
def _stopentry(self, update: Update, context: CallbackContext) -> None:
async def _stopentry(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /stop_buy.
Sets max_open_trades to 0 and gracefully sells all open trades
@ -1022,10 +1072,10 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_stopentry()
self._send_msg(f"Status: `{msg['status']}`")
await self._send_msg(f"Status: `{msg['status']}`")
@authorized_only
def _force_exit(self, update: Update, context: CallbackContext) -> None:
async def _force_exit(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /forceexit <id>.
Sells the given trade at current price
@ -1036,14 +1086,14 @@ class Telegram(RPCHandler):
if context.args:
trade_id = context.args[0]
self._force_exit_action(trade_id)
await self._force_exit_action(trade_id)
else:
fiat_currency = self._config.get('fiat_display_currency', '')
try:
statlist, _, _ = self._rpc._rpc_status_table(
self._config['stake_currency'], fiat_currency)
except RPCException:
self._send_msg(msg='No open trade found.')
await self._send_msg(msg='No open trade found.')
return
trades = []
for trade in statlist:
@ -1056,51 +1106,51 @@ class Telegram(RPCHandler):
buttons_aligned.append([InlineKeyboardButton(
text='Cancel', callback_data='force_exit__cancel')])
self._send_msg(msg="Which trade?", keyboard=buttons_aligned)
await self._send_msg(msg="Which trade?", keyboard=buttons_aligned)
def _force_exit_action(self, trade_id):
async def _force_exit_action(self, trade_id):
if trade_id != 'cancel':
try:
self._rpc._rpc_force_exit(trade_id)
except RPCException as e:
self._send_msg(str(e))
await self._send_msg(str(e))
def _force_exit_inline(self, update: Update, _: CallbackContext) -> None:
async def _force_exit_inline(self, update: Update, _: CallbackContext) -> None:
if update.callback_query:
query = update.callback_query
if query.data and '__' in query.data:
# Input data is "force_exit__<tradid|cancel>"
trade_id = query.data.split("__")[1].split(' ')[0]
if trade_id == 'cancel':
query.answer()
query.edit_message_text(text="Force exit canceled.")
await query.answer()
await query.edit_message_text(text="Force exit canceled.")
return
trade: Optional[Trade] = Trade.get_trades(trade_filter=Trade.id == trade_id).first()
query.answer()
await query.answer()
if trade:
query.edit_message_text(
await query.edit_message_text(
text=f"Manually exiting Trade #{trade_id}, {trade.pair}")
self._force_exit_action(trade_id)
await self._force_exit_action(trade_id)
else:
query.edit_message_text(text=f"Trade {trade_id} not found.")
await query.edit_message_text(text=f"Trade {trade_id} not found.")
def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
async def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
if pair != 'cancel':
try:
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
except RPCException as e:
logger.exception("Forcebuy error!")
self._send_msg(str(e), ParseMode.HTML)
await self._send_msg(str(e), ParseMode.HTML)
def _force_enter_inline(self, update: Update, _: CallbackContext) -> None:
async def _force_enter_inline(self, update: Update, _: CallbackContext) -> None:
if update.callback_query:
query = update.callback_query
if query.data and '_||_' in query.data:
pair, side = query.data.split('_||_')
order_side = SignalDirection(side)
query.answer()
query.edit_message_text(text=f"Manually entering {order_side} for {pair}")
self._force_enter_action(pair, None, order_side)
await query.answer()
await query.edit_message_text(text=f"Manually entering {order_side} for {pair}")
await self._force_enter_action(pair, None, order_side)
@staticmethod
def _layout_inline_keyboard(
@ -1113,7 +1163,7 @@ class Telegram(RPCHandler):
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
@authorized_only
def _force_enter(
async def _force_enter(
self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None:
"""
Handler for /forcelong <asset> <price> and `/forceshort <asset> <price>
@ -1125,7 +1175,7 @@ class Telegram(RPCHandler):
if context.args:
pair = context.args[0]
price = float(context.args[1]) if len(context.args) > 1 else None
self._force_enter_action(pair, price, order_side)
await self._force_enter_action(pair, price, order_side)
else:
whitelist = self._rpc._rpc_whitelist()['whitelist']
pair_buttons = [
@ -1135,12 +1185,12 @@ class Telegram(RPCHandler):
buttons_aligned = self._layout_inline_keyboard(pair_buttons)
buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')])
self._send_msg(msg="Which pair?",
keyboard=buttons_aligned,
query=update.callback_query)
await self._send_msg(msg="Which pair?",
keyboard=buttons_aligned,
query=update.callback_query)
@authorized_only
def _trades(self, update: Update, context: CallbackContext) -> None:
async def _trades(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /trades <n>
Returns last n recent trades.
@ -1169,10 +1219,10 @@ class Telegram(RPCHandler):
tablefmt='simple')
message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
+ (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
self._send_msg(message, parse_mode=ParseMode.HTML)
await self._send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _delete_trade(self, update: Update, context: CallbackContext) -> None:
async def _delete_trade(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /delete <id>.
Delete the given trade
@ -1184,13 +1234,13 @@ class Telegram(RPCHandler):
raise RPCException("Trade-id not set.")
trade_id = int(context.args[0])
msg = self._rpc._rpc_delete(trade_id)
self._send_msg(
await self._send_msg(
f"`{msg['result_msg']}`\n"
'Please make sure to take care of this asset on the exchange manually.'
)
@authorized_only
def _cancel_open_order(self, update: Update, context: CallbackContext) -> None:
async def _cancel_open_order(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /cancel_open_order <id>.
Cancel open order for tradeid
@ -1202,10 +1252,10 @@ class Telegram(RPCHandler):
raise RPCException("Trade-id not set.")
trade_id = int(context.args[0])
self._rpc._rpc_cancel_open_order(trade_id)
self._send_msg('Open order canceled.')
await self._send_msg('Open order canceled.')
@authorized_only
def _performance(self, update: Update, context: CallbackContext) -> None:
async def _performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /performance.
Shows a performance statistic from finished trades
@ -1223,17 +1273,17 @@ class Telegram(RPCHandler):
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
await self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_performance",
query=update.callback_query)
await self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_performance",
query=update.callback_query)
@authorized_only
def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
async def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /buys PAIR .
Shows a performance statistic from finished trades
@ -1255,17 +1305,17 @@ class Telegram(RPCHandler):
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
await self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_enter_tag_performance",
query=update.callback_query)
await self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_enter_tag_performance",
query=update.callback_query)
@authorized_only
def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None:
async def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /sells.
Shows a performance statistic from finished trades
@ -1287,17 +1337,17 @@ class Telegram(RPCHandler):
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
await self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_exit_reason_performance",
query=update.callback_query)
await self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_exit_reason_performance",
query=update.callback_query)
@authorized_only
def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None:
async def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /mix_tags.
Shows a performance statistic from finished trades
@ -1319,17 +1369,17 @@ class Telegram(RPCHandler):
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
await self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_mix_tag_performance",
query=update.callback_query)
await self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_mix_tag_performance",
query=update.callback_query)
@authorized_only
def _count(self, update: Update, context: CallbackContext) -> None:
async def _count(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /count.
Returns the number of trades running
@ -1343,19 +1393,19 @@ class Telegram(RPCHandler):
tablefmt='simple')
message = f"<pre>{message}</pre>"
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_count",
query=update.callback_query)
await self._send_msg(message, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_count",
query=update.callback_query)
@authorized_only
def _locks(self, update: Update, context: CallbackContext) -> None:
async def _locks(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /locks.
Returns the currently active locks
"""
rpc_locks = self._rpc._rpc_locks()
if not rpc_locks['locks']:
self._send_msg('No active locks.', parse_mode=ParseMode.HTML)
await self._send_msg('No active locks.', parse_mode=ParseMode.HTML)
for locks in chunks(rpc_locks['locks'], 25):
message = tabulate([[
@ -1367,10 +1417,10 @@ class Telegram(RPCHandler):
tablefmt='simple')
message = f"<pre>{escape(message)}</pre>"
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML)
await self._send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _delete_locks(self, update: Update, context: CallbackContext) -> None:
async def _delete_locks(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /delete_locks.
Returns the currently active locks
@ -1385,10 +1435,10 @@ class Telegram(RPCHandler):
pair = arg
self._rpc._rpc_delete_lock(lockid=lockid, pair=pair)
self._locks(update, context)
await self._locks(update, context)
@authorized_only
def _whitelist(self, update: Update, context: CallbackContext) -> None:
async def _whitelist(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /whitelist
Shows the currently active whitelist
@ -1405,39 +1455,39 @@ class Telegram(RPCHandler):
message += f"`{', '.join(whitelist['whitelist'])}`"
logger.debug(message)
self._send_msg(message)
await self._send_msg(message)
@authorized_only
def _blacklist(self, update: Update, context: CallbackContext) -> None:
async def _blacklist(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /blacklist
Shows the currently active blacklist
"""
self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args))
await self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args))
def send_blacklist_msg(self, blacklist: Dict):
async def send_blacklist_msg(self, blacklist: Dict):
errmsgs = []
for pair, error in blacklist['errors'].items():
errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`")
errmsgs.append(f"Error: {error['error_msg']}")
if errmsgs:
self._send_msg('\n'.join(errmsgs))
await self._send_msg('\n'.join(errmsgs))
message = f"Blacklist contains {blacklist['length']} pairs\n"
message += f"`{', '.join(blacklist['blacklist'])}`"
logger.debug(message)
self._send_msg(message)
await self._send_msg(message)
@authorized_only
def _blacklist_delete(self, update: Update, context: CallbackContext) -> None:
async def _blacklist_delete(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /bl_delete
Deletes pair(s) from current blacklist
"""
self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []))
await self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []))
@authorized_only
def _logs(self, update: Update, context: CallbackContext) -> None:
async def _logs(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /logs
Shows the latest logs
@ -1456,17 +1506,17 @@ class Telegram(RPCHandler):
escape_markdown(logrec[4], version=2))
if len(msgs + msg) + 10 >= MAX_MESSAGE_LENGTH:
# Send message immediately if it would become too long
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
msgs = msg + '\n'
else:
# Append message to messages to send
msgs += msg + '\n'
if msgs:
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
@authorized_only
def _edge(self, update: Update, context: CallbackContext) -> None:
async def _edge(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /edge
Shows information related to Edge
@ -1474,17 +1524,17 @@ class Telegram(RPCHandler):
edge_pairs = self._rpc._rpc_edge()
if not edge_pairs:
message = '<b>Edge only validated following pairs:</b>'
self._send_msg(message, parse_mode=ParseMode.HTML)
await self._send_msg(message, parse_mode=ParseMode.HTML)
for chunk in chunks(edge_pairs, 25):
edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple')
message = (f'<b>Edge only validated following pairs:</b>\n'
f'<pre>{edge_pairs_tab}</pre>')
self._send_msg(message, parse_mode=ParseMode.HTML)
await self._send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _help(self, update: Update, context: CallbackContext) -> None:
async def _help(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /help.
Show commands of the bot
@ -1528,7 +1578,8 @@ class Telegram(RPCHandler):
"------------\n"
"*/show_config:* `Show running configuration` \n"
"*/locks:* `Show currently locked pairs`\n"
"*/balance:* `Show account balance per currency`\n"
"*/balance:* `Show bot managed balance per currency`\n"
"*/balance total:* `Show account balance per currency`\n"
"*/logs [limit]:* `Show latest logs - defaults to 10` \n"
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
@ -1561,20 +1612,20 @@ class Telegram(RPCHandler):
"*/version:* `Show version`"
)
self._send_msg(message, parse_mode=ParseMode.MARKDOWN)
await self._send_msg(message, parse_mode=ParseMode.MARKDOWN)
@authorized_only
def _health(self, update: Update, context: CallbackContext) -> None:
async def _health(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /health
Shows the last process timestamp
"""
health = self._rpc.health()
message = f"Last process: `{health['last_process_loc']}`"
self._send_msg(message)
await self._send_msg(message)
@authorized_only
def _version(self, update: Update, context: CallbackContext) -> None:
async def _version(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /version.
Show version information
@ -1585,12 +1636,12 @@ class Telegram(RPCHandler):
strategy_version = self._rpc._freqtrade.strategy.version()
version_string = f'*Version:* `{__version__}`'
if strategy_version is not None:
version_string += f', *Strategy version: * `{strategy_version}`'
version_string += f'\n*Strategy version: * `{strategy_version}`'
self._send_msg(version_string)
await self._send_msg(version_string)
@authorized_only
def _show_config(self, update: Update, context: CallbackContext) -> None:
async def _show_config(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /show_config.
Show config information information
@ -1619,7 +1670,7 @@ class Telegram(RPCHandler):
else:
pa_info = "*Position adjustment:* Off\n"
self._send_msg(
await self._send_msg(
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
f"*Exchange:* `{val['exchange']}`\n"
f"*Market: * `{val['trading_mode']}`\n"
@ -1635,8 +1686,8 @@ class Telegram(RPCHandler):
f"*Current state:* `{val['state']}`"
)
def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "",
reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None:
async def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "",
reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None:
if reload_able:
reply_markup = InlineKeyboardMarkup([
[InlineKeyboardButton("Refresh", callback_data=callback_path)],
@ -1650,7 +1701,7 @@ class Telegram(RPCHandler):
message_id = query.message.message_id
try:
self._updater.bot.edit_message_text(
await self._app.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=msg,
@ -1665,12 +1716,12 @@ class Telegram(RPCHandler):
except TelegramError as telegram_err:
logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message)
def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
disable_notification: bool = False,
keyboard: Optional[List[List[InlineKeyboardButton]]] = None,
callback_path: str = "",
reload_able: bool = False,
query: Optional[CallbackQuery] = None) -> None:
async def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
disable_notification: bool = False,
keyboard: Optional[List[List[InlineKeyboardButton]]] = None,
callback_path: str = "",
reload_able: bool = False,
query: Optional[CallbackQuery] = None) -> None:
"""
Send given markdown message
:param msg: message
@ -1680,20 +1731,20 @@ class Telegram(RPCHandler):
"""
reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]
if query:
self._update_msg(query=query, msg=msg, parse_mode=parse_mode,
callback_path=callback_path, reload_able=reload_able)
await self._update_msg(query=query, msg=msg, parse_mode=parse_mode,
callback_path=callback_path, reload_able=reload_able)
return
if reload_able and self._config['telegram'].get('reload', True):
reply_markup = InlineKeyboardMarkup([
[InlineKeyboardButton("Refresh", callback_data=callback_path)]])
else:
if keyboard is not None:
reply_markup = InlineKeyboardMarkup(keyboard, resize_keyboard=True)
reply_markup = InlineKeyboardMarkup(keyboard)
else:
reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True)
try:
try:
self._updater.bot.send_message(
await self._app.bot.send_message(
self._config['telegram']['chat_id'],
text=msg,
parse_mode=parse_mode,
@ -1707,7 +1758,7 @@ class Telegram(RPCHandler):
'Telegram NetworkError: %s! Trying one more time.',
network_err.message
)
self._updater.bot.send_message(
await self._app.bot.send_message(
self._config['telegram']['chat_id'],
text=msg,
parse_mode=parse_mode,
@ -1721,7 +1772,7 @@ class Telegram(RPCHandler):
)
@authorized_only
def _changemarketdir(self, update: Update, context: CallbackContext) -> None:
async def _changemarketdir(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /marketdir.
Updates the bot's market_direction
@ -1744,14 +1795,14 @@ class Telegram(RPCHandler):
if new_market_dir is not None:
self._rpc._update_market_direction(new_market_dir)
self._send_msg("Successfully updated market direction"
f" from *{old_market_dir}* to *{new_market_dir}*.")
await self._send_msg("Successfully updated market direction"
f" from *{old_market_dir}* to *{new_market_dir}*.")
else:
raise RPCException("Invalid market direction provided. \n"
"Valid market directions: *long, short, even, none*")
elif context.args is not None and len(context.args) == 0:
old_market_dir = self._rpc._get_market_direction()
self._send_msg(f"Currently set market direction: *{old_market_dir}*")
await self._send_msg(f"Currently set market direction: *{old_market_dir}*")
else:
raise RPCException("Invalid usage of command /marketdir. \n"
"Usage: */marketdir [short | long | even | none]*")

View File

@ -44,8 +44,11 @@ class Webhook(RPCHandler):
def _get_value_dict(self, msg: RPCSendMsg) -> Optional[Dict[str, Any]]:
whconfig = self._config['webhook']
if msg['type'].value in whconfig:
# Explicit types should have priority
valuedict = whconfig.get(msg['type'].value)
# Deprecated 2022.10 - only keep generic method.
if msg['type'] in [RPCMessageType.ENTRY]:
elif msg['type'] in [RPCMessageType.ENTRY]:
valuedict = whconfig.get('webhookentry')
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
valuedict = whconfig.get('webhookentrycancel')
@ -62,9 +65,6 @@ class Webhook(RPCHandler):
RPCMessageType.EXCEPTION,
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,

View File

@ -618,7 +618,7 @@ class IStrategy(ABC, HyperStrategyMixin):
return df
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
metadata: Dict, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
@ -644,7 +644,8 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return dataframe
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
def feature_engineering_expand_basic(
self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
@ -673,7 +674,8 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
def feature_engineering_standard(
self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
@ -697,7 +699,7 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.

View File

@ -97,7 +97,7 @@ class FreqaiExampleHybridStrategy(IStrategy):
exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
metadata: Dict, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
@ -151,7 +151,8 @@ class FreqaiExampleHybridStrategy(IStrategy):
return dataframe
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
def feature_engineering_expand_basic(
self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
@ -183,7 +184,8 @@ class FreqaiExampleHybridStrategy(IStrategy):
dataframe["%-raw_price"] = dataframe["close"]
return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
def feature_engineering_standard(
self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
@ -209,7 +211,7 @@ class FreqaiExampleHybridStrategy(IStrategy):
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.

View File

@ -48,7 +48,7 @@ class FreqaiExampleStrategy(IStrategy):
[0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True)
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
metadata: Dict, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
@ -106,7 +106,8 @@ class FreqaiExampleStrategy(IStrategy):
return dataframe
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
def feature_engineering_expand_basic(
self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
@ -142,7 +143,8 @@ class FreqaiExampleStrategy(IStrategy):
dataframe["%-raw_price"] = dataframe["close"]
return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
def feature_engineering_standard(
self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
@ -172,7 +174,7 @@ class FreqaiExampleStrategy(IStrategy):
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.

View File

@ -11,6 +11,7 @@ from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config
from freqtrade.enums import RunMode, TradingMode
from freqtrade.exceptions import DependencyException
from freqtrade.exchange import Exchange
from freqtrade.misc import safe_value_fallback
from freqtrade.persistence import LocalTrade, Trade
@ -148,7 +149,7 @@ class Wallets:
# Position is not open ...
continue
size = self._exchange._contracts_to_amount(symbol, position['contracts'])
collateral = position['collateral'] or 0.0
collateral = safe_value_fallback(position, 'collateral', 'initialMargin', 0.0)
leverage = position['leverage']
self._positions[symbol] = PositionWallet(
symbol, position=size,

View File

@ -7,7 +7,7 @@
-r docs/requirements-docs.txt
coveralls==3.3.1
ruff==0.0.261
ruff==0.0.262
mypy==1.2.0
pre-commit==3.2.2
pytest==7.3.1
@ -18,8 +18,6 @@ pytest-random-order==1.1.0
isort==5.12.0
# For datetime mocking
time-machine==2.9.0
# fastapi testing
httpx==0.24.0
# Convert jupyter notebooks to markdown documents
nbconvert==7.3.1

View File

@ -5,4 +5,4 @@
scipy==1.10.1
scikit-learn==1.1.3
scikit-optimize==0.9.0
filelock==3.11.0
filelock==3.12.0

View File

@ -1,12 +1,14 @@
numpy==1.24.2
pandas==1.5.3
numpy==1.24.3
pandas==2.0.1
pandas-ta==0.3.14b
ccxt==3.0.69
ccxt==3.0.75
cryptography==40.0.2
aiohttp==3.8.4
SQLAlchemy==2.0.9
python-telegram-bot==13.15
SQLAlchemy==2.0.10
python-telegram-bot==20.2
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.23.3
arrow==1.2.3
cachetools==4.2.2
requests==2.28.2
@ -40,7 +42,7 @@ pydantic==1.10.7
uvicorn==0.21.1
pyjwt==2.6.0
aiofiles==23.1.0
psutil==5.9.4
psutil==5.9.5
# Support for colorized terminal output
colorama==0.4.6
@ -54,7 +56,7 @@ python-dateutil==2.8.2
schedule==1.2.0
#WS Messages
websockets==11.0.1
websockets==11.0.2
janus==1.0.0
ast-comments==1.0.1

View File

@ -50,7 +50,7 @@ function updateenv() {
SYS_ARCH=$(uname -m)
echo "pip install in-progress. Please wait..."
# Setuptools 65.5.0 is the last version that can install gym==0.21.0
${PYTHON} -m pip install --upgrade pip==23.0.1 wheel setuptools==65.5.1
${PYTHON} -m pip install --upgrade pip==23.0.1 wheel==0.38.4 setuptools==65.5.1
REQUIREMENTS_HYPEROPT=""
REQUIREMENTS_PLOT=""
REQUIREMENTS_FREQAI=""

View File

@ -3,7 +3,7 @@ import json
import logging
import re
from copy import deepcopy
from datetime import datetime, timedelta
from datetime import timedelta
from pathlib import Path
from typing import Optional
from unittest.mock import MagicMock, Mock, PropertyMock
@ -12,7 +12,6 @@ import arrow
import numpy as np
import pandas as pd
import pytest
from telegram import Chat, Message, Update
from freqtrade import constants
from freqtrade.commands import Arguments
@ -550,13 +549,6 @@ def get_default_conf_usdt(testdatadir):
return configuration
@pytest.fixture
def update():
_update = Update(0)
_update.message = Message(0, datetime.utcnow(), Chat(0, 0))
return _update
@pytest.fixture
def fee():
return MagicMock(return_value=0.0025)

View File

@ -9,7 +9,7 @@ import pytest
from arrow import Arrow
from freqtrade.configuration import TimeRange
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
from freqtrade.data import history
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data,
load_backtest_stats)
@ -470,6 +470,9 @@ def test__get_resample_from_period():
with pytest.raises(ValueError, match=r"Period noooo is not supported."):
_get_resample_from_period('noooo')
for period in BACKTEST_BREAKDOWNS:
assert isinstance(_get_resample_from_period(period), str)
def test_show_sorted_pairlist(testdatadir, default_conf, capsys):
filename = testdatadir / "backtest_results/backtest-result.json"

View File

@ -1,15 +1,18 @@
# pragma pylint: disable=missing-docstring, C0103
import logging
from importlib import import_module
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from sqlalchemy import create_engine, select, text
from sqlalchemy.schema import CreateTable
from freqtrade.constants import DEFAULT_DB_PROD_URL
from freqtrade.enums import TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import Trade, init_db
from freqtrade.persistence.base import ModelBase
from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
from freqtrade.persistence.models import PairLock
from tests.conftest import log_has
@ -411,3 +414,14 @@ def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
assert len(pairlocks) == 1
pairlocks[0].pair == 'ETH/BTC'
pairlocks[0].side == '*'
@pytest.mark.parametrize('dialect', [
'sqlite', 'postgresql', 'mysql', 'oracle', 'mssql',
])
def test_create_table_compiles(dialect):
dialect_mod = import_module(f"sqlalchemy.dialects.{dialect}")
for table in ModelBase.metadata.tables.values():
create_sql = str(CreateTable(table).compile(dialect=dialect_mod.dialect()))
assert 'CREATE TABLE' in create_sql

View File

@ -2481,7 +2481,7 @@ def test_select_filled_orders(fee):
@pytest.mark.usefixtures("init_persistence")
def test_order_to_ccxt(limit_buy_order_open):
def test_order_to_ccxt(limit_buy_order_open, limit_sell_order_usdt_open):
order = Order.parse_from_ccxt_object(limit_buy_order_open, 'mocked', 'buy')
order.ft_trade_id = 1
@ -2495,11 +2495,23 @@ def test_order_to_ccxt(limit_buy_order_open):
del raw_order['fee']
del raw_order['datetime']
del raw_order['info']
assert raw_order['stopPrice'] is None
del raw_order['stopPrice']
assert raw_order.get('stopPrice') is None
raw_order.pop('stopPrice', None)
del limit_buy_order_open['datetime']
assert raw_order == limit_buy_order_open
order1 = Order.parse_from_ccxt_object(limit_sell_order_usdt_open, 'mocked', 'sell')
order1.ft_order_side = 'stoploss'
order1.stop_price = order1.price * 0.9
order1.ft_trade_id = 1
order1.session.add(order1)
Order.session.commit()
order_resp1 = Order.order_by_id(limit_sell_order_usdt_open['id'])
raw_order1 = order_resp1.to_ccxt_object()
assert raw_order1.get('stopPrice') is not None
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize('data', [

View File

@ -546,51 +546,67 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
'free': 10.0,
'balance': 12.0,
'used': 2.0,
'bot_owned': 9.9, # available stake - reducing by reserved amount
'est_stake': 10.0, # In futures mode, "free" is used here.
'est_stake_bot': 9.9,
'stake': 'BTC',
'is_position': False,
'leverage': 1.0,
'position': 0.0,
'side': 'long',
'is_bot_managed': True,
},
{
'free': 1.0,
'balance': 5.0,
'currency': 'ETH',
'bot_owned': 0,
'est_stake': 0.30794,
'est_stake_bot': 0,
'used': 4.0,
'stake': 'BTC',
'is_position': False,
'leverage': 1.0,
'position': 0.0,
'side': 'long',
'is_bot_managed': False,
},
{
'free': 5.0,
'balance': 10.0,
'currency': 'USDT',
'bot_owned': 0,
'est_stake': 0.0011562404610161968,
'est_stake_bot': 0,
'used': 5.0,
'stake': 'BTC',
'is_position': False,
'leverage': 1.0,
'position': 0.0,
'side': 'long',
'is_bot_managed': False,
},
{
'free': 0.0,
'balance': 0.0,
'currency': 'ETH/USDT:USDT',
'est_stake': 20,
'est_stake_bot': 20,
'used': 0,
'stake': 'BTC',
'is_position': True,
'leverage': 5.0,
'position': 1000.0,
'side': 'short',
'is_bot_managed': True,
}
]
assert pytest.approx(result['total_bot']) == 29.9
assert pytest.approx(result['total']) == 30.309096
assert result['starting_capital'] == 10
# Very high starting capital ratio, because the futures position really has the wrong unit.
# TODO: improve this test (see comment above)
assert result['starting_capital_ratio'] == pytest.approx(1.98999999)
def test_rpc_start(mocker, default_conf) -> None:

View File

@ -283,7 +283,7 @@ def test_api__init__(default_conf, mocker):
"username": "TestUser",
"password": "testPass",
}})
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
mocker.patch('freqtrade.rpc.api_server.webserver.ApiServer.start_api', MagicMock())
apiserver = ApiServer(default_conf)
apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf)))
@ -341,7 +341,7 @@ def test_api_run(default_conf, mocker, caplog):
"username": "TestUser",
"password": "testPass",
}})
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
server_inst_mock = MagicMock()
server_inst_mock.run_in_thread = MagicMock()
@ -419,7 +419,7 @@ def test_api_cleanup(default_conf, mocker, caplog):
"username": "TestUser",
"password": "testPass",
}})
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
server_mock = MagicMock()
server_mock.cleanup = MagicMock()
@ -480,13 +480,18 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers):
'free': 12.0,
'balance': 12.0,
'used': 0.0,
'bot_owned': pytest.approx(11.879999),
'est_stake': 12.0,
'est_stake_bot': pytest.approx(11.879999),
'stake': 'BTC',
'is_position': False,
'leverage': 1.0,
'position': 0.0,
'side': 'long',
'is_bot_managed': True,
}
assert response['total'] == 12.159513094
assert response['total_bot'] == pytest.approx(11.879999)
assert 'starting_capital' in response
assert 'starting_capital_fiat' in response
assert 'starting_capital_pct' in response
@ -1872,7 +1877,7 @@ def test_api_ws_send_msg(default_conf, mocker, caplog):
"password": _TEST_PASS,
"ws_token": _TEST_WS_TOKEN
}})
mocker.patch('freqtrade.rpc.telegram.Updater')
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api')
apiserver = ApiServer(default_conf)
apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf)))

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,10 @@ def get_webhook_dict() -> dict:
"enabled": True,
"url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
"webhookentry": {
# Intentionally broken, as "entry" should have priority.
"value1": "Buying {pair55555}",
},
"entry": {
"value1": "Buying {pair}",
"value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}",
@ -89,15 +93,15 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookentry"]["value1"].format(**msg))
default_conf["webhook"]["entry"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookentry"]["value2"].format(**msg))
default_conf["webhook"]["entry"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookentry"]["value3"].format(**msg))
default_conf["webhook"]["entry"]["value3"].format(**msg))
assert (msg_mock.call_args[0][0]["value4"] ==
default_conf["webhook"]["webhookentry"]["value4"].format(**msg))
default_conf["webhook"]["entry"]["value4"].format(**msg))
assert (msg_mock.call_args[0][0]["value5"] ==
default_conf["webhook"]["webhookentry"]["value5"].format(**msg))
default_conf["webhook"]["entry"]["value5"].format(**msg))
# Test short
msg_mock.reset_mock()
@ -116,15 +120,15 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookentry"]["value1"].format(**msg))
default_conf["webhook"]["entry"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookentry"]["value2"].format(**msg))
default_conf["webhook"]["entry"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookentry"]["value3"].format(**msg))
default_conf["webhook"]["entry"]["value3"].format(**msg))
assert (msg_mock.call_args[0][0]["value4"] ==
default_conf["webhook"]["webhookentry"]["value4"].format(**msg))
default_conf["webhook"]["entry"]["value4"].format(**msg))
assert (msg_mock.call_args[0][0]["value5"] ==
default_conf["webhook"]["webhookentry"]["value5"].format(**msg))
default_conf["webhook"]["entry"]["value5"].format(**msg))
# Test buy cancel
msg_mock.reset_mock()
@ -328,6 +332,7 @@ def test_send_msg_webhook(default_conf, mocker):
def test_exception_send_msg(default_conf, mocker, caplog):
default_conf["webhook"] = get_webhook_dict()
del default_conf["webhook"]["entry"]
del default_conf["webhook"]["webhookentry"]
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)

View File

@ -2107,6 +2107,7 @@ def test_enter_positions(mocker, default_conf_usdt, return_value, side_effect,
assert mock_ct.call_count == len(default_conf_usdt['exchange']['pair_whitelist'])
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("is_short", [False, True])
def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
@ -2115,12 +2116,33 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog
mocker.patch(f'{EXMS}.fetch_order', return_value=limit_order[entry_side(is_short)])
mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[])
# TODO: should not be magicmock
trade = MagicMock()
trade.is_short = is_short
trade.open_order_id = '123'
trade.open_fee = 0.001
order_id = '123'
trade = Trade(
open_order_id=order_id,
pair='ETH/USDT',
fee_open=0.001,
fee_close=0.001,
open_rate=0.01,
open_date=arrow.utcnow().datetime,
stake_amount=0.01,
amount=11,
exchange="binance",
is_short=is_short,
leverage=1,
)
trade.orders.append(Order(
ft_order_side=entry_side(is_short),
price=0.01,
ft_pair=trade.pair,
ft_amount=trade.amount,
ft_price=trade.open_rate,
order_id=order_id,
))
Trade.session.add(trade)
Trade.commit()
trades = [trade]
freqtrade.wallets.update()
n = freqtrade.exit_positions(trades)
assert n == 0
# Test amount not modified by fee-logic
@ -2133,17 +2155,40 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog
assert gra.call_count == 0
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("is_short", [False, True])
def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog, is_short) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
order = limit_order[entry_side(is_short)]
mocker.patch(f'{EXMS}.fetch_order', return_value=order)
# TODO: should not be magicmock
trade = MagicMock()
trade.is_short = is_short
order_id = '123'
trade = Trade(
open_order_id=order_id,
pair='ETH/USDT',
fee_open=0.001,
fee_close=0.001,
open_rate=0.01,
open_date=arrow.utcnow().datetime,
stake_amount=0.01,
amount=11,
exchange="binance",
is_short=is_short,
leverage=1,
)
trade.orders.append(Order(
ft_order_side=entry_side(is_short),
price=0.01,
ft_pair=trade.pair,
ft_amount=trade.amount,
ft_price=trade.open_rate,
order_id=order_id,
))
trade.open_order_id = None
trade.pair = 'ETH/USDT'
Trade.session.add(trade)
Trade.commit()
freqtrade.wallets.update()
trades = [trade]
# Test raise of DependencyException exception