mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge branch 'freqtrade:develop' into develop
This commit is contained in:
commit
ce979b21f9
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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}')"
|
||||
|
||||
|
|
|
@ -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**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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"`.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
""" Freqtrade bot """
|
||||
__version__ = '2023.4.dev'
|
||||
__version__ = '2023.5.dev'
|
||||
|
||||
if 'dev' in __version__:
|
||||
from pathlib import Path
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]*")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
2
setup.sh
2
setup.sh
|
@ -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=""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', [
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user