mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
commit
9aac367534
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
@ -425,7 +425,7 @@ jobs:
|
|||
python setup.py sdist bdist_wheel
|
||||
|
||||
- name: Publish to PyPI (Test)
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.5
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.6
|
||||
if: (github.event_name == 'release')
|
||||
with:
|
||||
user: __token__
|
||||
|
@ -433,7 +433,7 @@ jobs:
|
|||
repository_url: https://test.pypi.org/legacy/
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.5
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.6
|
||||
if: (github.event_name == 'release')
|
||||
with:
|
||||
user: __token__
|
||||
|
|
|
@ -15,10 +15,10 @@ repos:
|
|||
additional_dependencies:
|
||||
- types-cachetools==5.3.0.5
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.28.11.17
|
||||
- types-requests==2.30.0.0
|
||||
- types-tabulate==0.9.0.2
|
||||
- types-python-dateutil==2.8.19.12
|
||||
- SQLAlchemy==2.0.9
|
||||
- SQLAlchemy==2.0.12
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
|
@ -30,7 +30,7 @@ repos:
|
|||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.0.255'
|
||||
rev: 'v0.0.263'
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
|
|
@ -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
|
||||
&& pip install --upgrade pip wheel
|
||||
|
||||
# Install TA-lib
|
||||
COPY build_helpers/* /tmp/
|
||||
|
|
|
@ -29,7 +29,7 @@ If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl`
|
|||
`user_data/backtest_results` folder.
|
||||
|
||||
To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-analysis` command
|
||||
with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`):
|
||||
with `--analysis-groups` option provided with space-separated arguments:
|
||||
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4 5
|
||||
|
@ -39,6 +39,7 @@ This command will read from the last backtesting results. The `--analysis-groups
|
|||
used to specify the various tabular outputs showing the profit fo each group or trade,
|
||||
ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4):
|
||||
|
||||
* 0: overall winrate and profit summary by enter_tag
|
||||
* 1: profit summaries grouped by enter_tag
|
||||
* 2: profit summaries grouped by enter_tag and exit_tag
|
||||
* 3: profit summaries grouped by pair and enter_tag
|
||||
|
@ -115,3 +116,38 @@ For example, if your backtest timerange was `20220101-20221231` but you only wan
|
|||
```bash
|
||||
freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
||||
```
|
||||
|
||||
### Printing out rejected signals
|
||||
|
||||
Use the `--rejected-signals` option to print out rejected signals.
|
||||
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c <config.json> --rejected-signals
|
||||
```
|
||||
|
||||
### Writing tables to CSV
|
||||
|
||||
Some of the tabular outputs can become large, so printing them out to the terminal is not preferable.
|
||||
Use the `--analysis-to-csv` option to disable printing out of tables to standard out and write them to CSV files.
|
||||
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv
|
||||
```
|
||||
|
||||
By default this will write one file per output table you specified in the `backtesting-analysis` command, e.g.
|
||||
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --rejected-signals --analysis-groups 0 1
|
||||
```
|
||||
|
||||
This will write to `user_data/backtest_results`:
|
||||
|
||||
* rejected_signals.csv
|
||||
* group_0.csv
|
||||
* group_1.csv
|
||||
|
||||
To override where the files will be written, also specify the `--analysis-csv-path` option.
|
||||
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --analysis-csv-path another/data/path/
|
||||
```
|
||||
|
|
|
@ -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')
|
||||
|
@ -394,3 +395,21 @@ Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. T
|
|||
return dataframe
|
||||
```
|
||||
To see a full example, you can refer to the [classifier test strategy class](https://github.com/freqtrade/freqtrade/blob/develop/tests/strategy/strats/freqai_test_classifier.py).
|
||||
|
||||
|
||||
#### Improving performance with `torch.compile()`
|
||||
|
||||
Torch provides a `torch.compile()` method that can be used to improve performance for specific GPU hardware. More details can be found [here](https://pytorch.org/tutorials/intermediate/torch_compile_tutorial.html). In brief, you simply wrap your `model` in `torch.compile()`:
|
||||
|
||||
|
||||
```python
|
||||
model = PyTorchMLPModel(
|
||||
input_dim=n_features,
|
||||
output_dim=1,
|
||||
**self.model_kwargs
|
||||
)
|
||||
model.to(self.device)
|
||||
model = torch.compile(model)
|
||||
```
|
||||
|
||||
Then proceed to use the model as normal. Keep in mind that doing this will remove eager execution, which means errors and tracebacks will not be informative.
|
||||
|
|
|
@ -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"`.
|
||||
|
|
|
@ -85,6 +85,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||
| `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[<shared layers>, dict(vf=[<non-shared value network layers>], pi=[<non-shared policy network layers>])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each.
|
||||
| `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting. <br> **Datatype:** bool. <br> Default: `False`.
|
||||
| `drop_ohlc_from_features` | Do not include the normalized ohlc data in the feature set passed to the agent during training (ohlc will still be used for driving the environment in all cases) <br> **Datatype:** Boolean. <br> **Default:** `False`
|
||||
| `progress_bar` | Display a progress bar with the current progress, elapsed time and estimated remaining time. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
|
||||
### PyTorch parameters
|
||||
|
||||
|
@ -113,5 +114,5 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||
|------------|-------------|
|
||||
| | **Extraneous parameters**
|
||||
| `freqai.keras` | If the selected model makes use of Keras (typical for TensorFlow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
| `freqai.conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
||||
| `freqai.conv_width` | The width of a neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
||||
| `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -30,12 +30,6 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
|||
!!! Warning "Up-to-date clock"
|
||||
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
|
||||
|
||||
!!! Error "Running setup.py install for gym did not run successfully."
|
||||
If you get an error related with gym we suggest you to downgrade setuptools it to version 65.5.0 you can do it with the following command:
|
||||
```bash
|
||||
pip install setuptools==65.5.0
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
## Requirements
|
||||
|
@ -242,6 +236,7 @@ source .env/bin/activate
|
|||
|
||||
```bash
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install -r requirements.txt
|
||||
python3 -m pip install -e .
|
||||
```
|
||||
|
||||
|
|
|
@ -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==1.4.3
|
||||
mkdocs-material==9.1.10
|
||||
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):
|
||||
|
@ -227,8 +227,8 @@ for val in self.buy_ema_short.range:
|
|||
f'ema_short_{val}': ta.EMA(dataframe, timeperiod=val)
|
||||
}))
|
||||
|
||||
# Append columns to existing dataframe
|
||||
merged_frame = pd.concat(frames, axis=1)
|
||||
# Combine all dataframes, and reassign the original dataframe column
|
||||
dataframe = pd.concat(frames, axis=1)
|
||||
```
|
||||
|
||||
Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existant.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -723,6 +723,9 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--logfile FILE] [-V]
|
|||
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
|
||||
[--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]]
|
||||
[--timerange YYYYMMDD-[YYYYMMDD]]
|
||||
[--rejected]
|
||||
[--analysis-to-csv]
|
||||
[--analysis-csv-path PATH]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
|
@ -736,19 +739,27 @@ optional arguments:
|
|||
pair and enter_tag, 4: by pair, enter_ and exit_tag
|
||||
(this can get quite large)
|
||||
--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]
|
||||
Comma separated list of entry signals to analyse.
|
||||
Default: all. e.g. 'entry_tag_a,entry_tag_b'
|
||||
Space separated list of entry signals to analyse.
|
||||
Default: all. e.g. 'entry_tag_a entry_tag_b'
|
||||
--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]
|
||||
Comma separated list of exit signals to analyse.
|
||||
Space separated list of exit signals to analyse.
|
||||
Default: all. e.g.
|
||||
'exit_tag_a,roi,stop_loss,trailing_stop_loss'
|
||||
'exit_tag_a roi stop_loss trailing_stop_loss'
|
||||
--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]
|
||||
Comma separated list of indicators to analyse. e.g.
|
||||
'close,rsi,bb_lowerband,profit_abs'
|
||||
Space separated list of indicators to analyse. e.g.
|
||||
'close rsi bb_lowerband profit_abs'
|
||||
--timerange YYYYMMDD-[YYYYMMDD]
|
||||
Timerange to filter trades for analysis,
|
||||
start inclusive, end exclusive. e.g.
|
||||
20220101-20220201
|
||||
--rejected
|
||||
Print out rejected trades table
|
||||
--analysis-to-csv
|
||||
Write out tables to individual CSVs, by default to
|
||||
'user_data/backtest_results' unless '--analysis-csv-path' is given.
|
||||
--analysis-csv-path [PATH]
|
||||
Optional path where individual CSVs will be written. If not used,
|
||||
CSVs will be written to 'user_data/backtest_results'.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
@ -106,7 +106,8 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
|||
"disableparamexport", "backtest_breakdown"]
|
||||
|
||||
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
|
||||
"exit_reason_list", "indicator_list", "timerange"]
|
||||
"exit_reason_list", "indicator_list", "timerange",
|
||||
"analysis_rejected", "analysis_to_csv", "analysis_csv_path"]
|
||||
|
||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels",
|
||||
|
|
|
@ -636,30 +636,45 @@ AVAILABLE_CLI_OPTIONS = {
|
|||
"4: by pair, enter_ and exit_tag (this can get quite large), "
|
||||
"5: by exit_tag"),
|
||||
nargs='+',
|
||||
default=['0', '1', '2'],
|
||||
default=[],
|
||||
choices=['0', '1', '2', '3', '4', '5'],
|
||||
),
|
||||
"enter_reason_list": Arg(
|
||||
"--enter-reason-list",
|
||||
help=("Comma separated list of entry signals to analyse. Default: all. "
|
||||
"e.g. 'entry_tag_a,entry_tag_b'"),
|
||||
help=("Space separated list of entry signals to analyse. Default: all. "
|
||||
"e.g. 'entry_tag_a entry_tag_b'"),
|
||||
nargs='+',
|
||||
default=['all'],
|
||||
),
|
||||
"exit_reason_list": Arg(
|
||||
"--exit-reason-list",
|
||||
help=("Comma separated list of exit signals to analyse. Default: all. "
|
||||
"e.g. 'exit_tag_a,roi,stop_loss,trailing_stop_loss'"),
|
||||
help=("Space separated list of exit signals to analyse. Default: all. "
|
||||
"e.g. 'exit_tag_a roi stop_loss trailing_stop_loss'"),
|
||||
nargs='+',
|
||||
default=['all'],
|
||||
),
|
||||
"indicator_list": Arg(
|
||||
"--indicator-list",
|
||||
help=("Comma separated list of indicators to analyse. "
|
||||
"e.g. 'close,rsi,bb_lowerband,profit_abs'"),
|
||||
help=("Space separated list of indicators to analyse. "
|
||||
"e.g. 'close rsi bb_lowerband profit_abs'"),
|
||||
nargs='+',
|
||||
default=[],
|
||||
),
|
||||
"analysis_rejected": Arg(
|
||||
'--rejected-signals',
|
||||
help='Analyse rejected signals',
|
||||
action='store_true',
|
||||
),
|
||||
"analysis_to_csv": Arg(
|
||||
'--analysis-to-csv',
|
||||
help='Save selected analysis tables to individual CSVs',
|
||||
action='store_true',
|
||||
),
|
||||
"analysis_csv_path": Arg(
|
||||
'--analysis-csv-path',
|
||||
help=("Specify a path to save the analysis CSVs "
|
||||
"if --analysis-to-csv is enabled. Default: user_data/basktesting_results/"),
|
||||
),
|
||||
"freqaimodel": Arg(
|
||||
'--freqaimodel',
|
||||
help='Specify a custom freqaimodels.',
|
||||
|
|
|
@ -465,6 +465,15 @@ class Configuration:
|
|||
self._args_to_config(config, argname='timerange',
|
||||
logstring='Filter trades by timerange: {}')
|
||||
|
||||
self._args_to_config(config, argname='analysis_rejected',
|
||||
logstring='Analyse rejected signals: {}')
|
||||
|
||||
self._args_to_config(config, argname='analysis_to_csv',
|
||||
logstring='Store analysis tables to CSV: {}')
|
||||
|
||||
self._args_to_config(config, argname='analysis_csv_path',
|
||||
logstring='Path to store analysis CSVs: {}')
|
||||
|
||||
def _process_runmode(self, config: Config) -> None:
|
||||
|
||||
self._args_to_config(config, argname='dry_run',
|
||||
|
|
|
@ -600,6 +600,7 @@ CONF_SCHEMA = {
|
|||
"policy_type": {"type": "string", "default": "MlpPolicy"},
|
||||
"net_arch": {"type": "array", "default": [128, 128]},
|
||||
"randomize_starting_position": {"type": "boolean", "default": False},
|
||||
"progress_bar": {"type": "boolean", "default": True},
|
||||
"model_reward_parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import joblib
|
||||
import pandas as pd
|
||||
|
@ -15,22 +16,31 @@ from freqtrade.exceptions import OperationalException
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _load_signal_candles(backtest_dir: Path):
|
||||
def _load_backtest_analysis_data(backtest_dir: Path, name: str):
|
||||
if backtest_dir.is_dir():
|
||||
scpf = Path(backtest_dir,
|
||||
Path(get_latest_backtest_filename(backtest_dir)).stem + "_signals.pkl"
|
||||
Path(get_latest_backtest_filename(backtest_dir)).stem + "_" + name + ".pkl"
|
||||
)
|
||||
else:
|
||||
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl")
|
||||
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl")
|
||||
|
||||
try:
|
||||
with scpf.open("rb") as scp:
|
||||
signal_candles = joblib.load(scp)
|
||||
logger.info(f"Loaded signal candles: {str(scpf)}")
|
||||
loaded_data = joblib.load(scp)
|
||||
logger.info(f"Loaded {name} candles: {str(scpf)}")
|
||||
except Exception as e:
|
||||
logger.error("Cannot load signal candles from pickled results: ", e)
|
||||
logger.error(f"Cannot load {name} data from pickled results: ", e)
|
||||
return None
|
||||
|
||||
return signal_candles
|
||||
return loaded_data
|
||||
|
||||
|
||||
def _load_rejected_signals(backtest_dir: Path):
|
||||
return _load_backtest_analysis_data(backtest_dir, "rejected")
|
||||
|
||||
|
||||
def _load_signal_candles(backtest_dir: Path):
|
||||
return _load_backtest_analysis_data(backtest_dir, "signals")
|
||||
|
||||
|
||||
def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles):
|
||||
|
@ -43,9 +53,7 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
|
|||
for pair in pairlist:
|
||||
if pair in signal_candles[strategy_name]:
|
||||
analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
|
||||
pair,
|
||||
trades,
|
||||
signal_candles[strategy_name][pair])
|
||||
pair, trades, signal_candles[strategy_name][pair])
|
||||
except Exception as e:
|
||||
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
|
||||
|
||||
|
@ -85,7 +93,7 @@ def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles:
|
|||
return pd.DataFrame()
|
||||
|
||||
|
||||
def _do_group_table_output(bigdf, glist):
|
||||
def _do_group_table_output(bigdf, glist, csv_path: Path, to_csv=False, ):
|
||||
for g in glist:
|
||||
# 0: summary wins/losses grouped by enter tag
|
||||
if g == "0":
|
||||
|
@ -116,7 +124,8 @@ def _do_group_table_output(bigdf, glist):
|
|||
|
||||
sortcols = ['total_num_buys']
|
||||
|
||||
_print_table(new, sortcols, show_index=True)
|
||||
_print_table(new, sortcols, show_index=True, name="Group 0:",
|
||||
to_csv=to_csv, csv_path=csv_path)
|
||||
|
||||
else:
|
||||
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
|
||||
|
@ -154,11 +163,24 @@ def _do_group_table_output(bigdf, glist):
|
|||
new['mean_profit_pct'] = new['mean_profit_pct'] * 100
|
||||
new['total_profit_pct'] = new['total_profit_pct'] * 100
|
||||
|
||||
_print_table(new, sortcols)
|
||||
_print_table(new, sortcols, name=f"Group {g}:",
|
||||
to_csv=to_csv, csv_path=csv_path)
|
||||
else:
|
||||
logger.warning("Invalid group mask specified.")
|
||||
|
||||
|
||||
def _do_rejected_signals_output(rejected_signals_df: pd.DataFrame,
|
||||
to_csv: bool = False, csv_path=None) -> None:
|
||||
cols = ['pair', 'date', 'enter_tag']
|
||||
sortcols = ['date', 'pair', 'enter_tag']
|
||||
_print_table(rejected_signals_df[cols],
|
||||
sortcols,
|
||||
show_index=False,
|
||||
name="Rejected Signals:",
|
||||
to_csv=to_csv,
|
||||
csv_path=csv_path)
|
||||
|
||||
|
||||
def _select_rows_within_dates(df, timerange=None, df_date_col: str = 'date'):
|
||||
if timerange:
|
||||
if timerange.starttype == 'date':
|
||||
|
@ -192,38 +214,64 @@ def prepare_results(analysed_trades, stratname,
|
|||
return res_df
|
||||
|
||||
|
||||
def print_results(res_df, analysis_groups, indicator_list):
|
||||
def print_results(res_df: pd.DataFrame, analysis_groups: List[str], indicator_list: List[str],
|
||||
csv_path: Path, rejected_signals=None, to_csv=False):
|
||||
if res_df.shape[0] > 0:
|
||||
if analysis_groups:
|
||||
_do_group_table_output(res_df, analysis_groups)
|
||||
_do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path)
|
||||
|
||||
if rejected_signals is not None:
|
||||
if rejected_signals.empty:
|
||||
print("There were no rejected signals.")
|
||||
else:
|
||||
_do_rejected_signals_output(rejected_signals, to_csv=to_csv, csv_path=csv_path)
|
||||
|
||||
# NB this can be large for big dataframes!
|
||||
if "all" in indicator_list:
|
||||
print(res_df)
|
||||
elif indicator_list is not None:
|
||||
_print_table(res_df,
|
||||
show_index=False,
|
||||
name="Indicators:",
|
||||
to_csv=to_csv,
|
||||
csv_path=csv_path)
|
||||
elif indicator_list is not None and indicator_list:
|
||||
available_inds = []
|
||||
for ind in indicator_list:
|
||||
if ind in res_df:
|
||||
available_inds.append(ind)
|
||||
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
||||
_print_table(res_df[ilist], sortcols=['exit_reason'], show_index=False)
|
||||
_print_table(res_df[ilist],
|
||||
sortcols=['exit_reason'],
|
||||
show_index=False,
|
||||
name="Indicators:",
|
||||
to_csv=to_csv,
|
||||
csv_path=csv_path)
|
||||
else:
|
||||
print("\\No trades to show")
|
||||
|
||||
|
||||
def _print_table(df, sortcols=None, show_index=False):
|
||||
def _print_table(df: pd.DataFrame, sortcols=None, *, show_index=False, name=None,
|
||||
to_csv=False, csv_path: Path):
|
||||
if (sortcols is not None):
|
||||
data = df.sort_values(sortcols)
|
||||
else:
|
||||
data = df
|
||||
|
||||
print(
|
||||
tabulate(
|
||||
data,
|
||||
headers='keys',
|
||||
tablefmt='psql',
|
||||
showindex=show_index
|
||||
if to_csv:
|
||||
safe_name = Path(csv_path, name.lower().replace(" ", "_").replace(":", "") + ".csv")
|
||||
data.to_csv(safe_name)
|
||||
print(f"Saved {name} to {safe_name}")
|
||||
else:
|
||||
if name is not None:
|
||||
print(name)
|
||||
|
||||
print(
|
||||
tabulate(
|
||||
data,
|
||||
headers='keys',
|
||||
tablefmt='psql',
|
||||
showindex=show_index
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def process_entry_exit_reasons(config: Config):
|
||||
|
@ -232,6 +280,11 @@ def process_entry_exit_reasons(config: Config):
|
|||
enter_reason_list = config.get('enter_reason_list', ["all"])
|
||||
exit_reason_list = config.get('exit_reason_list', ["all"])
|
||||
indicator_list = config.get('indicator_list', [])
|
||||
do_rejected = config.get('analysis_rejected', False)
|
||||
to_csv = config.get('analysis_to_csv', False)
|
||||
csv_path = Path(config.get('analysis_csv_path', config['exportfilename']))
|
||||
if to_csv and not csv_path.is_dir():
|
||||
raise OperationalException(f"Specified directory {csv_path} does not exist.")
|
||||
|
||||
timerange = TimeRange.parse_timerange(None if config.get(
|
||||
'timerange') is None else str(config.get('timerange')))
|
||||
|
@ -241,8 +294,16 @@ def process_entry_exit_reasons(config: Config):
|
|||
for strategy_name, results in backtest_stats['strategy'].items():
|
||||
trades = load_backtest_data(config['exportfilename'], strategy_name)
|
||||
|
||||
if not trades.empty:
|
||||
if trades is not None and not trades.empty:
|
||||
signal_candles = _load_signal_candles(config['exportfilename'])
|
||||
|
||||
rej_df = None
|
||||
if do_rejected:
|
||||
rejected_signals_dict = _load_rejected_signals(config['exportfilename'])
|
||||
rej_df = prepare_results(rejected_signals_dict, strategy_name,
|
||||
enter_reason_list, exit_reason_list,
|
||||
timerange=timerange)
|
||||
|
||||
analysed_trades_dict = _process_candles_and_indicators(
|
||||
config['exchange']['pair_whitelist'], strategy_name,
|
||||
trades, signal_candles)
|
||||
|
@ -253,7 +314,10 @@ def process_entry_exit_reasons(config: Config):
|
|||
|
||||
print_results(res_df,
|
||||
analysis_groups,
|
||||
indicator_list)
|
||||
indicator_list,
|
||||
rejected_signals=rej_df,
|
||||
to_csv=to_csv,
|
||||
csv_path=csv_path)
|
||||
|
||||
except ValueError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
|
|
@ -6,6 +6,7 @@ from freqtrade.exchange.exchange import Exchange
|
|||
from freqtrade.exchange.binance import Binance
|
||||
from freqtrade.exchange.bitpanda import Bitpanda
|
||||
from freqtrade.exchange.bittrex import Bittrex
|
||||
from freqtrade.exchange.bitvavo import Bitvavo
|
||||
from freqtrade.exchange.bybit import Bybit
|
||||
from freqtrade.exchange.coinbasepro import Coinbasepro
|
||||
from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
23
freqtrade/exchange/bitvavo.py
Normal file
23
freqtrade/exchange/bitvavo.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
"""Kucoin exchange subclass."""
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Bitvavo(Exchange):
|
||||
"""Bitvavo exchange class.
|
||||
|
||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||
|
||||
Please note that this exchange is not included in the list of exchanges
|
||||
officially supported by the Freqtrade development team. So some features
|
||||
may still not work as expected.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"ohlcv_candle_limit": 1440,
|
||||
}
|
|
@ -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.
|
||||
|
@ -2371,12 +2375,12 @@ class Exchange:
|
|||
# Must fetch the leverage tiers for each market separately
|
||||
# * This is slow(~45s) on Okx, makes ~90 api calls to load all linear swap markets
|
||||
markets = self.markets
|
||||
symbols = []
|
||||
|
||||
for symbol, market in markets.items():
|
||||
symbols = [
|
||||
symbol for symbol, market in markets.items()
|
||||
if (self.market_is_future(market)
|
||||
and market['quote'] == self._config['stake_currency']):
|
||||
symbols.append(symbol)
|
||||
and market['quote'] == self._config['stake_currency'])
|
||||
]
|
||||
|
||||
tiers: Dict[str, List[Dict]] = {}
|
||||
|
||||
|
@ -2396,25 +2400,26 @@ class Exchange:
|
|||
else:
|
||||
logger.info("Using cached leverage_tiers.")
|
||||
|
||||
async def gather_results():
|
||||
async def gather_results(input_coro):
|
||||
return await asyncio.gather(*input_coro, return_exceptions=True)
|
||||
|
||||
for input_coro in chunks(coros, 100):
|
||||
|
||||
with self._loop_lock:
|
||||
results = self.loop.run_until_complete(gather_results())
|
||||
results = self.loop.run_until_complete(gather_results(input_coro))
|
||||
|
||||
for symbol, res in results:
|
||||
tiers[symbol] = res
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning(f"Leverage tier exception: {repr(res)}")
|
||||
continue
|
||||
symbol, tier = res
|
||||
tiers[symbol] = tier
|
||||
if len(coros) > 0:
|
||||
self.cache_leverage_tiers(tiers, self._config['stake_currency'])
|
||||
logger.info(f"Done initializing {len(symbols)} markets.")
|
||||
|
||||
return tiers
|
||||
else:
|
||||
return {}
|
||||
else:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def cache_leverage_tiers(self, tiers: Dict[str, List[Dict]], stake_currency: str) -> None:
|
||||
|
||||
|
@ -2430,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:
|
||||
|
@ -2892,8 +2900,8 @@ class Exchange:
|
|||
if nominal_value >= tier['minNotional']:
|
||||
return (tier['maintenanceMarginRate'], tier['maintAmt'])
|
||||
|
||||
raise OperationalException("nominal value can not be lower than 0")
|
||||
raise ExchangeError("nominal value can not be lower than 0")
|
||||
# The lowest notional_floor for any pair in fetch_leverage_tiers is always 0 because it
|
||||
# describes the min amt for a tier, and the lowest tier will always go down to 0
|
||||
else:
|
||||
raise OperationalException(f"Cannot get maintenance ratio using {self.name}")
|
||||
raise ExchangeError(f"Cannot get maintenance ratio using {self.name}")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
from enum import Enum
|
||||
|
||||
from gym import spaces
|
||||
from gymnasium import spaces
|
||||
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||
|
||||
|
@ -94,9 +94,12 @@ class Base3ActionRLEnv(BaseEnvironment):
|
|||
|
||||
observation = self._get_observation()
|
||||
|
||||
# user can play with time if they want
|
||||
truncated = False
|
||||
|
||||
self._update_history(info)
|
||||
|
||||
return observation, step_reward, self._done, info
|
||||
return observation, step_reward, self._done, truncated, info
|
||||
|
||||
def is_tradesignal(self, action: int) -> bool:
|
||||
"""
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
from enum import Enum
|
||||
|
||||
from gym import spaces
|
||||
from gymnasium import spaces
|
||||
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||
|
||||
|
@ -96,9 +96,12 @@ class Base4ActionRLEnv(BaseEnvironment):
|
|||
|
||||
observation = self._get_observation()
|
||||
|
||||
# user can play with time if they want
|
||||
truncated = False
|
||||
|
||||
self._update_history(info)
|
||||
|
||||
return observation, step_reward, self._done, info
|
||||
return observation, step_reward, self._done, truncated, info
|
||||
|
||||
def is_tradesignal(self, action: int) -> bool:
|
||||
"""
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
from enum import Enum
|
||||
|
||||
from gym import spaces
|
||||
from gymnasium import spaces
|
||||
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||
|
||||
|
@ -101,10 +101,12 @@ class Base5ActionRLEnv(BaseEnvironment):
|
|||
)
|
||||
|
||||
observation = self._get_observation()
|
||||
# user can play with time if they want
|
||||
truncated = False
|
||||
|
||||
self._update_history(info)
|
||||
|
||||
return observation, step_reward, self._done, info
|
||||
return observation, step_reward, self._done, truncated, info
|
||||
|
||||
def is_tradesignal(self, action: int) -> bool:
|
||||
"""
|
||||
|
|
|
@ -4,11 +4,11 @@ from abc import abstractmethod
|
|||
from enum import Enum
|
||||
from typing import Optional, Type, Union
|
||||
|
||||
import gym
|
||||
import gymnasium as gym
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from gym import spaces
|
||||
from gym.utils import seeding
|
||||
from gymnasium import spaces
|
||||
from gymnasium.utils import seeding
|
||||
from pandas import DataFrame
|
||||
|
||||
|
||||
|
@ -127,6 +127,14 @@ class BaseEnvironment(gym.Env):
|
|||
self.history: dict = {}
|
||||
self.trade_history: list = []
|
||||
|
||||
def get_attr(self, attr: str):
|
||||
"""
|
||||
Returns the attribute of the environment
|
||||
:param attr: attribute to return
|
||||
:return: attribute
|
||||
"""
|
||||
return getattr(self, attr)
|
||||
|
||||
@abstractmethod
|
||||
def set_action_space(self):
|
||||
"""
|
||||
|
@ -203,7 +211,7 @@ class BaseEnvironment(gym.Env):
|
|||
self.close_trade_profit = []
|
||||
self._total_unrealized_profit = 1
|
||||
|
||||
return self._get_observation()
|
||||
return self._get_observation(), self.history
|
||||
|
||||
@abstractmethod
|
||||
def step(self, action: int):
|
||||
|
|
|
@ -6,7 +6,7 @@ from datetime import datetime, timezone
|
|||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Optional, Tuple, Type, Union
|
||||
|
||||
import gym
|
||||
import gymnasium as gym
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import pandas as pd
|
||||
|
@ -16,13 +16,13 @@ from pandas import DataFrame
|
|||
from stable_baselines3.common.callbacks import EvalCallback
|
||||
from stable_baselines3.common.monitor import Monitor
|
||||
from stable_baselines3.common.utils import set_random_seed
|
||||
from stable_baselines3.common.vec_env import SubprocVecEnv
|
||||
from stable_baselines3.common.vec_env import SubprocVecEnv, VecMonitor
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.freqai.freqai_interface import IFreqaiModel
|
||||
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseActions, Positions
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseActions, BaseEnvironment, Positions
|
||||
from freqtrade.freqai.RL.TensorboardCallback import TensorboardCallback
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
|
@ -46,8 +46,8 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||
'cpu_count', 1), max(int(self.max_system_threads / 2), 1))
|
||||
th.set_num_threads(self.max_threads)
|
||||
self.reward_params = self.freqai_info['rl_config']['model_reward_parameters']
|
||||
self.train_env: Union[SubprocVecEnv, Type[gym.Env]] = gym.Env()
|
||||
self.eval_env: Union[SubprocVecEnv, Type[gym.Env]] = gym.Env()
|
||||
self.train_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env()
|
||||
self.eval_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env()
|
||||
self.eval_callback: Optional[EvalCallback] = None
|
||||
self.model_type = self.freqai_info['rl_config']['model_type']
|
||||
self.rl_config = self.freqai_info['rl_config']
|
||||
|
@ -431,9 +431,8 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||
return 0.
|
||||
|
||||
|
||||
def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int,
|
||||
def make_env(MyRLEnv: Type[BaseEnvironment], env_id: str, rank: int,
|
||||
seed: int, train_df: DataFrame, price: DataFrame,
|
||||
monitor: bool = False,
|
||||
env_info: Dict[str, Any] = {}) -> Callable:
|
||||
"""
|
||||
Utility function for multiprocessed env.
|
||||
|
@ -450,8 +449,7 @@ def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int,
|
|||
|
||||
env = MyRLEnv(df=train_df, prices=price, id=env_id, seed=seed + rank,
|
||||
**env_info)
|
||||
if monitor:
|
||||
env = Monitor(env)
|
||||
|
||||
return env
|
||||
set_random_seed(seed)
|
||||
return _init
|
||||
|
|
|
@ -3,8 +3,9 @@ from typing import Any, Dict, Type, Union
|
|||
|
||||
from stable_baselines3.common.callbacks import BaseCallback
|
||||
from stable_baselines3.common.logger import HParam
|
||||
from stable_baselines3.common.vec_env import VecEnv
|
||||
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseActions, BaseEnvironment
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseActions
|
||||
|
||||
|
||||
class TensorboardCallback(BaseCallback):
|
||||
|
@ -12,11 +13,13 @@ class TensorboardCallback(BaseCallback):
|
|||
Custom callback for plotting additional values in tensorboard and
|
||||
episodic summary reports.
|
||||
"""
|
||||
# Override training_env type to fix type errors
|
||||
training_env: Union[VecEnv, None] = None
|
||||
|
||||
def __init__(self, verbose=1, actions: Type[Enum] = BaseActions):
|
||||
super().__init__(verbose)
|
||||
self.model: Any = None
|
||||
self.logger = None # type: Any
|
||||
self.training_env: BaseEnvironment = None # type: ignore
|
||||
self.logger: Any = None
|
||||
self.actions: Type[Enum] = actions
|
||||
|
||||
def _on_training_start(self) -> None:
|
||||
|
@ -44,6 +47,8 @@ class TensorboardCallback(BaseCallback):
|
|||
def _on_step(self) -> bool:
|
||||
|
||||
local_info = self.locals["infos"][0]
|
||||
if self.training_env is None:
|
||||
return True
|
||||
tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0]
|
||||
|
||||
for metric in local_info:
|
||||
|
|
|
@ -45,6 +45,7 @@ class BasePyTorchClassifier(BasePyTorchModel):
|
|||
) -> Tuple[DataFrame, npt.NDArray[np.int_]]:
|
||||
"""
|
||||
Filter the prediction features data and predict with it.
|
||||
:param dk: dk: The datakitchen object
|
||||
:param unfiltered_df: Full dataframe for the current backtest period.
|
||||
:return:
|
||||
:pred_df: dataframe containing the predictions
|
||||
|
@ -74,11 +75,14 @@ class BasePyTorchClassifier(BasePyTorchModel):
|
|||
dk.data_dictionary["prediction_features"],
|
||||
device=self.device
|
||||
)
|
||||
self.model.model.eval()
|
||||
logits = self.model.model(x)
|
||||
probs = F.softmax(logits, dim=-1)
|
||||
predicted_classes = torch.argmax(probs, dim=-1)
|
||||
predicted_classes_str = self.decode_class_names(predicted_classes)
|
||||
pred_df_prob = DataFrame(probs.detach().numpy(), columns=class_names)
|
||||
# used .tolist to convert probs into an iterable, in this way Tensors
|
||||
# are automatically moved to the CPU first if necessary.
|
||||
pred_df_prob = DataFrame(probs.detach().tolist(), columns=class_names)
|
||||
pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]])
|
||||
pred_df = pd.concat([pred_df, pred_df_prob], axis=1)
|
||||
return (pred_df, dk.do_predict)
|
||||
|
|
|
@ -27,6 +27,7 @@ class BasePyTorchModel(IFreqaiModel, ABC):
|
|||
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
test_size = self.freqai_info.get('data_split_parameters', {}).get('test_size')
|
||||
self.splits = ["train", "test"] if test_size != 0 else ["train"]
|
||||
self.window_size = self.freqai_info.get("conv_width", 1)
|
||||
|
||||
def train(
|
||||
self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs
|
||||
|
|
|
@ -44,7 +44,7 @@ class BasePyTorchRegressor(BasePyTorchModel):
|
|||
dk.data_dictionary["prediction_features"],
|
||||
device=self.device
|
||||
)
|
||||
self.model.model.eval()
|
||||
y = self.model.model(x)
|
||||
y = y.cpu()
|
||||
pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]])
|
||||
pred_df = DataFrame(y.detach().tolist(), columns=[dk.label_list[0]])
|
||||
return (pred_df, dk.do_predict)
|
||||
|
|
|
@ -80,6 +80,7 @@ class IFreqaiModel(ABC):
|
|||
if self.keras and self.ft_params.get("DI_threshold", 0):
|
||||
self.ft_params["DI_threshold"] = 0
|
||||
logger.warning("DI threshold is not configured for Keras models yet. Deactivating.")
|
||||
|
||||
self.CONV_WIDTH = self.freqai_info.get('conv_width', 1)
|
||||
if self.ft_params.get("inlier_metric_window", 0):
|
||||
self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2
|
||||
|
@ -242,8 +243,8 @@ class IFreqaiModel(ABC):
|
|||
new_trained_timerange, pair, strategy, dk, data_load_timerange
|
||||
)
|
||||
except Exception as msg:
|
||||
logger.warning(f"Training {pair} raised exception {msg.__class__.__name__}. "
|
||||
f"Message: {msg}, skipping.")
|
||||
logger.exception(f"Training {pair} raised exception {msg.__class__.__name__}. "
|
||||
f"Message: {msg}, skipping.")
|
||||
|
||||
self.train_timer('stop', pair)
|
||||
|
||||
|
@ -306,10 +307,11 @@ class IFreqaiModel(ABC):
|
|||
if dk.check_if_backtest_prediction_is_valid(len_backtest_df):
|
||||
if check_features:
|
||||
self.dd.load_metadata(dk)
|
||||
dataframe_dummy_features = self.dk.use_strategy_to_populate_indicators(
|
||||
df_fts = self.dk.use_strategy_to_populate_indicators(
|
||||
strategy, prediction_dataframe=dataframe.tail(1), pair=pair
|
||||
)
|
||||
dk.find_features(dataframe_dummy_features)
|
||||
df_fts = dk.remove_special_chars_from_feature_names(df_fts)
|
||||
dk.find_features(df_fts)
|
||||
self.check_if_feature_list_matches_strategy(dk)
|
||||
check_features = False
|
||||
append_df = dk.get_backtesting_prediction()
|
||||
|
@ -489,9 +491,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"
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
from typing import Any, Dict, Tuple
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import pandas as pd
|
||||
import torch
|
||||
|
||||
from freqtrade.freqai.base_models.BasePyTorchRegressor import BasePyTorchRegressor
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.freqai.torch.PyTorchDataConvertor import (DefaultPyTorchDataConvertor,
|
||||
PyTorchDataConvertor)
|
||||
from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchTransformerTrainer
|
||||
from freqtrade.freqai.torch.PyTorchTransformerModel import PyTorchTransformerModel
|
||||
|
||||
|
||||
class PyTorchTransformerRegressor(BasePyTorchRegressor):
|
||||
"""
|
||||
This class implements the fit method of IFreqaiModel.
|
||||
in the fit method we initialize the model and trainer objects.
|
||||
the only requirement from the model is to be aligned to PyTorchRegressor
|
||||
predict method that expects the model to predict tensor of type float.
|
||||
the trainer defines the training loop.
|
||||
|
||||
parameters are passed via `model_training_parameters` under the freqai
|
||||
section in the config file. e.g:
|
||||
{
|
||||
...
|
||||
"freqai": {
|
||||
...
|
||||
"model_training_parameters" : {
|
||||
"learning_rate": 3e-4,
|
||||
"trainer_kwargs": {
|
||||
"max_iters": 5000,
|
||||
"batch_size": 64,
|
||||
"max_n_eval_batches": null,
|
||||
"window_size": 10
|
||||
},
|
||||
"model_kwargs": {
|
||||
"hidden_dim": 512,
|
||||
"dropout_percent": 0.2,
|
||||
"n_layer": 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@property
|
||||
def data_convertor(self) -> PyTorchDataConvertor:
|
||||
return DefaultPyTorchDataConvertor(target_tensor_type=torch.float)
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
config = self.freqai_info.get("model_training_parameters", {})
|
||||
self.learning_rate: float = config.get("learning_rate", 3e-4)
|
||||
self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {})
|
||||
self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {})
|
||||
|
||||
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
"""
|
||||
User sets up the training and test data to fit their desired model here
|
||||
:param data_dictionary: the dictionary holding all data for train, test,
|
||||
labels, weights
|
||||
:param dk: The datakitchen object for the current coin/model
|
||||
"""
|
||||
|
||||
n_features = data_dictionary["train_features"].shape[-1]
|
||||
n_labels = data_dictionary["train_labels"].shape[-1]
|
||||
model = PyTorchTransformerModel(
|
||||
input_dim=n_features,
|
||||
output_dim=n_labels,
|
||||
time_window=self.window_size,
|
||||
**self.model_kwargs
|
||||
)
|
||||
model.to(self.device)
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate)
|
||||
criterion = torch.nn.MSELoss()
|
||||
init_model = self.get_init_model(dk.pair)
|
||||
trainer = PyTorchTransformerTrainer(
|
||||
model=model,
|
||||
optimizer=optimizer,
|
||||
criterion=criterion,
|
||||
device=self.device,
|
||||
init_model=init_model,
|
||||
data_convertor=self.data_convertor,
|
||||
window_size=self.window_size,
|
||||
**self.trainer_kwargs,
|
||||
)
|
||||
trainer.fit(data_dictionary, self.splits)
|
||||
return trainer
|
||||
|
||||
def predict(
|
||||
self, unfiltered_df: pd.DataFrame, dk: FreqaiDataKitchen, **kwargs
|
||||
) -> Tuple[pd.DataFrame, npt.NDArray[np.int_]]:
|
||||
"""
|
||||
Filter the prediction features data and predict with it.
|
||||
:param unfiltered_df: Full dataframe for the current backtest period.
|
||||
:return:
|
||||
:pred_df: dataframe containing the predictions
|
||||
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
|
||||
data (NaNs) or felt uncertain about data (PCA and DI index)
|
||||
"""
|
||||
|
||||
dk.find_features(unfiltered_df)
|
||||
filtered_df, _ = dk.filter_features(
|
||||
unfiltered_df, dk.training_features_list, training_filter=False
|
||||
)
|
||||
filtered_df = dk.normalize_data_from_metadata(filtered_df)
|
||||
dk.data_dictionary["prediction_features"] = filtered_df
|
||||
|
||||
self.data_cleaning_predict(dk)
|
||||
x = self.data_convertor.convert_x(
|
||||
dk.data_dictionary["prediction_features"],
|
||||
device=self.device
|
||||
)
|
||||
# if user is asking for multiple predictions, slide the window
|
||||
# along the tensor
|
||||
x = x.unsqueeze(0)
|
||||
# create empty torch tensor
|
||||
self.model.model.eval()
|
||||
yb = torch.empty(0)
|
||||
if x.shape[1] > 1:
|
||||
ws = self.window_size
|
||||
for i in range(0, x.shape[1] - ws):
|
||||
xb = x[:, i:i + ws, :]
|
||||
y = self.model.model(xb)
|
||||
yb = torch.cat((yb, y), dim=0)
|
||||
else:
|
||||
yb = self.model.model(x)
|
||||
|
||||
yb = yb.cpu().squeeze()
|
||||
pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list)
|
||||
pred_df = dk.denormalize_labels_from_metadata(pred_df)
|
||||
|
||||
if x.shape[1] > 1:
|
||||
zeros_df = pd.DataFrame(np.zeros((x.shape[1] - len(pred_df), len(pred_df.columns))),
|
||||
columns=pred_df.columns)
|
||||
pred_df = pd.concat([zeros_df, pred_df], axis=0, ignore_index=True)
|
||||
return (pred_df, dk.do_predict)
|
|
@ -1,11 +1,12 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Type
|
||||
|
||||
import torch as th
|
||||
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment
|
||||
from freqtrade.freqai.RL.BaseReinforcementLearningModel import BaseReinforcementLearningModel
|
||||
|
||||
|
||||
|
@ -71,7 +72,8 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
|
|||
|
||||
model.learn(
|
||||
total_timesteps=int(total_timesteps),
|
||||
callback=[self.eval_callback, self.tensorboard_callback]
|
||||
callback=[self.eval_callback, self.tensorboard_callback],
|
||||
progress_bar=self.rl_config.get('progress_bar', False)
|
||||
)
|
||||
|
||||
if Path(dk.data_path / "best_model.zip").is_file():
|
||||
|
@ -83,7 +85,9 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
|
|||
|
||||
return model
|
||||
|
||||
class MyRLEnv(Base5ActionRLEnv):
|
||||
MyRLEnv: Type[BaseEnvironment]
|
||||
|
||||
class MyRLEnv(Base5ActionRLEnv): # type: ignore[no-redef]
|
||||
"""
|
||||
User can override any function in BaseRLEnv and gym.Env. Here the user
|
||||
sets a custom reward based on profit and trade duration.
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import Any, Dict
|
|||
|
||||
from pandas import DataFrame
|
||||
from stable_baselines3.common.callbacks import EvalCallback
|
||||
from stable_baselines3.common.vec_env import SubprocVecEnv
|
||||
from stable_baselines3.common.vec_env import SubprocVecEnv, VecMonitor
|
||||
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
|
||||
|
@ -41,22 +41,25 @@ class ReinforcementLearner_multiproc(ReinforcementLearner):
|
|||
|
||||
env_info = self.pack_env_dict(dk.pair)
|
||||
|
||||
eval_freq = len(train_df) // self.max_threads
|
||||
|
||||
env_id = "train_env"
|
||||
self.train_env = SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1,
|
||||
train_df, prices_train,
|
||||
monitor=True,
|
||||
env_info=env_info) for i
|
||||
in range(self.max_threads)])
|
||||
self.train_env = VecMonitor(SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1,
|
||||
train_df, prices_train,
|
||||
env_info=env_info) for i
|
||||
in range(self.max_threads)]))
|
||||
|
||||
eval_env_id = 'eval_env'
|
||||
self.eval_env = SubprocVecEnv([make_env(self.MyRLEnv, eval_env_id, i, 1,
|
||||
test_df, prices_test,
|
||||
monitor=True,
|
||||
env_info=env_info) for i
|
||||
in range(self.max_threads)])
|
||||
self.eval_env = VecMonitor(SubprocVecEnv([make_env(self.MyRLEnv, eval_env_id, i, 1,
|
||||
test_df, prices_test,
|
||||
env_info=env_info) for i
|
||||
in range(self.max_threads)]))
|
||||
|
||||
self.eval_callback = EvalCallback(self.eval_env, deterministic=True,
|
||||
render=False, eval_freq=len(train_df),
|
||||
render=False, eval_freq=eval_freq,
|
||||
best_model_save_path=str(dk.data_path))
|
||||
|
||||
# TENSORBOARD CALLBACK DOES NOT RECOMMENDED TO USE WITH MULTIPLE ENVS,
|
||||
# IT WILL RETURN FALSE INFORMATIONS, NEVERTHLESS NOT THREAD SAFE WITH SB3!!!
|
||||
actions = self.train_env.env_method("get_actions")[0]
|
||||
self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
import pandas as pd
|
||||
import torch
|
||||
|
@ -12,14 +12,14 @@ class PyTorchDataConvertor(ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]:
|
||||
def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor:
|
||||
"""
|
||||
:param df: "*_features" dataframe.
|
||||
:param device: The device to use for training (e.g. 'cpu', 'cuda').
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]:
|
||||
def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor:
|
||||
"""
|
||||
:param df: "*_labels" dataframe.
|
||||
:param device: The device to use for training (e.g. 'cpu', 'cuda').
|
||||
|
@ -45,14 +45,14 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor):
|
|||
self._target_tensor_type = target_tensor_type
|
||||
self._squeeze_target_tensor = squeeze_target_tensor
|
||||
|
||||
def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]:
|
||||
def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor:
|
||||
x = torch.from_numpy(df.values).float()
|
||||
if device:
|
||||
x = x.to(device)
|
||||
|
||||
return [x]
|
||||
return x
|
||||
|
||||
def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]:
|
||||
def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor:
|
||||
y = torch.from_numpy(df.values)
|
||||
|
||||
if self._target_tensor_type:
|
||||
|
@ -64,4 +64,4 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor):
|
|||
if device:
|
||||
y = y.to(device)
|
||||
|
||||
return [y]
|
||||
return y
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import logging
|
||||
from typing import List
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
@ -47,8 +46,8 @@ class PyTorchMLPModel(nn.Module):
|
|||
self.relu = nn.ReLU()
|
||||
self.dropout = nn.Dropout(p=dropout_percent)
|
||||
|
||||
def forward(self, tensors: List[torch.Tensor]) -> torch.Tensor:
|
||||
x: torch.Tensor = tensors[0]
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
# x: torch.Tensor = tensors[0]
|
||||
x = self.relu(self.input_layer(x))
|
||||
x = self.dropout(x)
|
||||
x = self.blocks(x)
|
||||
|
|
|
@ -12,6 +12,8 @@ from torch.utils.data import DataLoader, TensorDataset
|
|||
from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor
|
||||
from freqtrade.freqai.torch.PyTorchTrainerInterface import PyTorchTrainerInterface
|
||||
|
||||
from .datasets import WindowDataset
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -26,6 +28,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||
init_model: Dict,
|
||||
data_convertor: PyTorchDataConvertor,
|
||||
model_meta_data: Dict[str, Any] = {},
|
||||
window_size: int = 1,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
|
@ -52,6 +55,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||
self.batch_size: int = kwargs.get("batch_size", 64)
|
||||
self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None)
|
||||
self.data_convertor = data_convertor
|
||||
self.window_size: int = window_size
|
||||
if init_model:
|
||||
self.load_from_checkpoint(init_model)
|
||||
|
||||
|
@ -75,16 +79,15 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||
batch_size=self.batch_size,
|
||||
n_iters=self.max_iters
|
||||
)
|
||||
self.model.train()
|
||||
for epoch in range(1, epochs + 1):
|
||||
# training
|
||||
losses = []
|
||||
for i, batch_data in enumerate(data_loaders_dictionary["train"]):
|
||||
|
||||
for tensor in batch_data:
|
||||
tensor.to(self.device)
|
||||
|
||||
xb = batch_data[:-1]
|
||||
yb = batch_data[-1]
|
||||
xb, yb = batch_data
|
||||
xb.to(self.device)
|
||||
yb.to(self.device)
|
||||
yb_pred = self.model(xb)
|
||||
loss = self.criterion(yb_pred, yb)
|
||||
|
||||
|
@ -120,12 +123,10 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||
if max_n_eval_batches and i > max_n_eval_batches:
|
||||
n_batches += 1
|
||||
break
|
||||
xb, yb = batch_data
|
||||
xb.to(self.device)
|
||||
yb.to(self.device)
|
||||
|
||||
for tensor in batch_data:
|
||||
tensor.to(self.device)
|
||||
|
||||
xb = batch_data[:-1]
|
||||
yb = batch_data[-1]
|
||||
yb_pred = self.model(xb)
|
||||
loss = self.criterion(yb_pred, yb)
|
||||
losses.append(loss.item())
|
||||
|
@ -145,7 +146,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||
for split in splits:
|
||||
x = self.data_convertor.convert_x(data_dictionary[f"{split}_features"], self.device)
|
||||
y = self.data_convertor.convert_y(data_dictionary[f"{split}_labels"], self.device)
|
||||
dataset = TensorDataset(*x, *y)
|
||||
dataset = TensorDataset(x, y)
|
||||
data_loader = DataLoader(
|
||||
dataset,
|
||||
batch_size=self.batch_size,
|
||||
|
@ -206,3 +207,33 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||
self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
|
||||
self.model_meta_data = checkpoint["model_meta_data"]
|
||||
return self
|
||||
|
||||
|
||||
class PyTorchTransformerTrainer(PyTorchModelTrainer):
|
||||
"""
|
||||
Creating a trainer for the Transformer model.
|
||||
"""
|
||||
|
||||
def create_data_loaders_dictionary(
|
||||
self,
|
||||
data_dictionary: Dict[str, pd.DataFrame],
|
||||
splits: List[str]
|
||||
) -> Dict[str, DataLoader]:
|
||||
"""
|
||||
Converts the input data to PyTorch tensors using a data loader.
|
||||
"""
|
||||
data_loader_dictionary = {}
|
||||
for split in splits:
|
||||
x = self.data_convertor.convert_x(data_dictionary[f"{split}_features"], self.device)
|
||||
y = self.data_convertor.convert_y(data_dictionary[f"{split}_labels"], self.device)
|
||||
dataset = WindowDataset(x, y, self.window_size)
|
||||
data_loader = DataLoader(
|
||||
dataset,
|
||||
batch_size=self.batch_size,
|
||||
shuffle=False,
|
||||
drop_last=True,
|
||||
num_workers=0,
|
||||
)
|
||||
data_loader_dictionary[split] = data_loader
|
||||
|
||||
return data_loader_dictionary
|
||||
|
|
93
freqtrade/freqai/torch/PyTorchTransformerModel.py
Normal file
93
freqtrade/freqai/torch/PyTorchTransformerModel.py
Normal file
|
@ -0,0 +1,93 @@
|
|||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
"""
|
||||
The architecture is based on the paper “Attention Is All You Need”.
|
||||
Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez,
|
||||
Lukasz Kaiser, and Illia Polosukhin. 2017.
|
||||
"""
|
||||
|
||||
|
||||
class PyTorchTransformerModel(nn.Module):
|
||||
"""
|
||||
A transformer approach to time series modeling using positional encoding.
|
||||
The architecture is based on the paper “Attention Is All You Need”.
|
||||
Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez,
|
||||
Lukasz Kaiser, and Illia Polosukhin. 2017.
|
||||
"""
|
||||
|
||||
def __init__(self, input_dim: int = 7, output_dim: int = 7, hidden_dim=1024,
|
||||
n_layer=2, dropout_percent=0.1, time_window=10, nhead=8):
|
||||
super().__init__()
|
||||
self.time_window = time_window
|
||||
# ensure the input dimension to the transformer is divisible by nhead
|
||||
self.dim_val = input_dim - (input_dim % nhead)
|
||||
self.input_net = nn.Sequential(
|
||||
nn.Dropout(dropout_percent), nn.Linear(input_dim, self.dim_val)
|
||||
)
|
||||
|
||||
# Encode the timeseries with Positional encoding
|
||||
self.positional_encoding = PositionalEncoding(d_model=self.dim_val, max_len=self.dim_val)
|
||||
|
||||
# Define the encoder block of the Transformer
|
||||
self.encoder_layer = nn.TransformerEncoderLayer(
|
||||
d_model=self.dim_val, nhead=nhead, dropout=dropout_percent, batch_first=True)
|
||||
self.transformer = nn.TransformerEncoder(self.encoder_layer, num_layers=n_layer)
|
||||
|
||||
# the pseudo decoding FC
|
||||
self.output_net = nn.Sequential(
|
||||
nn.Linear(self.dim_val * time_window, int(hidden_dim)),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(dropout_percent),
|
||||
nn.Linear(int(hidden_dim), int(hidden_dim / 2)),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(dropout_percent),
|
||||
nn.Linear(int(hidden_dim / 2), int(hidden_dim / 4)),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(dropout_percent),
|
||||
nn.Linear(int(hidden_dim / 4), output_dim)
|
||||
)
|
||||
|
||||
def forward(self, x, mask=None, add_positional_encoding=True):
|
||||
"""
|
||||
Args:
|
||||
x: Input features of shape [Batch, SeqLen, input_dim]
|
||||
mask: Mask to apply on the attention outputs (optional)
|
||||
add_positional_encoding: If True, we add the positional encoding to the input.
|
||||
Might not be desired for some tasks.
|
||||
"""
|
||||
x = self.input_net(x)
|
||||
if add_positional_encoding:
|
||||
x = self.positional_encoding(x)
|
||||
x = self.transformer(x, mask=mask)
|
||||
x = x.reshape(-1, 1, self.time_window * x.shape[-1])
|
||||
x = self.output_net(x)
|
||||
return x
|
||||
|
||||
|
||||
class PositionalEncoding(torch.nn.Module):
|
||||
def __init__(self, d_model, max_len=5000):
|
||||
"""
|
||||
Args
|
||||
d_model: Hidden dimensionality of the input.
|
||||
max_len: Maximum length of a sequence to expect.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# Create matrix of [SeqLen, HiddenDim] representing the positional encoding
|
||||
# for max_len inputs
|
||||
pe = torch.zeros(max_len, d_model)
|
||||
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
|
||||
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
|
||||
pe[:, 0::2] = torch.sin(position * div_term)
|
||||
pe[:, 1::2] = torch.cos(position * div_term)
|
||||
pe = pe.unsqueeze(0)
|
||||
|
||||
self.register_buffer("pe", pe, persistent=False)
|
||||
|
||||
def forward(self, x):
|
||||
x = x + self.pe[:, : x.size(1)]
|
||||
return x
|
19
freqtrade/freqai/torch/datasets.py
Normal file
19
freqtrade/freqai/torch/datasets.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
import torch
|
||||
|
||||
|
||||
class WindowDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, xs, ys, window_size):
|
||||
self.xs = xs
|
||||
self.ys = ys
|
||||
self.window_size = window_size
|
||||
|
||||
def __len__(self):
|
||||
return len(self.xs) - self.window_size
|
||||
|
||||
def __getitem__(self, index):
|
||||
idx_rev = len(self.xs) - self.window_size - index - 1
|
||||
window_x = self.xs[idx_rev:idx_rev + self.window_size, :]
|
||||
# Beware of indexing, these two window_x and window_y are aimed at the same row!
|
||||
# this is what happens when you use :
|
||||
window_y = self.ys[idx_rev + self.window_size - 1, :].unsqueeze(0)
|
||||
return window_x, window_y
|
|
@ -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
|
||||
|
@ -1720,10 +1721,8 @@ class FreqtradeBot(LoggingMixin):
|
|||
else:
|
||||
trade.exit_order_status = reason
|
||||
|
||||
order = trade.select_order_by_order_id(order_id)
|
||||
if not order:
|
||||
raise DependencyException(
|
||||
f"Order_obj not found for {order_id}. This should not have happened.")
|
||||
order_or_none = trade.select_order_by_order_id(order_id)
|
||||
order = self.order_obj_or_raise(order_id, order_or_none)
|
||||
|
||||
profit_rate: float = trade.safe_close_rate
|
||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||
|
@ -1764,6 +1763,12 @@ class FreqtradeBot(LoggingMixin):
|
|||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def order_obj_or_raise(self, order_id: str, order_obj: Optional[Order]) -> Order:
|
||||
if not order_obj:
|
||||
raise DependencyException(
|
||||
f"Order_obj not found for {order_id}. This should not have happened.")
|
||||
return order_obj
|
||||
|
||||
#
|
||||
# Common update trade state methods
|
||||
#
|
||||
|
@ -1802,10 +1807,8 @@ class FreqtradeBot(LoggingMixin):
|
|||
# Handling of this will happen in check_handle_timedout.
|
||||
return True
|
||||
|
||||
order_obj = trade.select_order_by_order_id(order_id)
|
||||
if not order_obj:
|
||||
raise DependencyException(
|
||||
f"Order_obj not found for {order_id}. This should not have happened.")
|
||||
order_obj_or_none = trade.select_order_by_order_id(order_id)
|
||||
order_obj = self.order_obj_or_raise(order_id, order_obj_or_none)
|
||||
|
||||
self.handle_order_fee(trade, order_obj, order)
|
||||
|
||||
|
@ -1823,16 +1826,18 @@ class FreqtradeBot(LoggingMixin):
|
|||
# Must also run for partial exits
|
||||
# TODO: Margin will need to use interest_rate as well.
|
||||
# interest_rate = self.exchange.get_interest_rate()
|
||||
trade.set_liquidation_price(self.exchange.get_liquidation_price(
|
||||
pair=trade.pair,
|
||||
open_rate=trade.open_rate,
|
||||
is_short=trade.is_short,
|
||||
amount=trade.amount,
|
||||
stake_amount=trade.stake_amount,
|
||||
leverage=trade.leverage,
|
||||
wallet_balance=trade.stake_amount,
|
||||
))
|
||||
|
||||
try:
|
||||
trade.set_liquidation_price(self.exchange.get_liquidation_price(
|
||||
pair=trade.pair,
|
||||
open_rate=trade.open_rate,
|
||||
is_short=trade.is_short,
|
||||
amount=trade.amount,
|
||||
stake_amount=trade.stake_amount,
|
||||
leverage=trade.leverage,
|
||||
wallet_balance=trade.stake_amount,
|
||||
))
|
||||
except DependencyException:
|
||||
logger.warning('Unable to calculate liquidation price')
|
||||
# Updating wallets when order is closed
|
||||
self.wallets.update()
|
||||
Trade.commit()
|
||||
|
|
|
@ -9,7 +9,6 @@ from copy import deepcopy
|
|||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import pandas as pd
|
||||
from numpy import nan
|
||||
from pandas import DataFrame
|
||||
|
||||
|
@ -28,8 +27,10 @@ from freqtrade.exchange import (amount_to_contract_precision, price_to_precision
|
|||
from freqtrade.mixins import LoggingMixin
|
||||
from freqtrade.optimize.backtest_caching import get_strategy_run_id
|
||||
from freqtrade.optimize.bt_progress import BTProgress
|
||||
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
|
||||
store_backtest_signal_candles,
|
||||
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_rejected_signals,
|
||||
generate_trade_signal_candles,
|
||||
show_backtest_results,
|
||||
store_backtest_analysis_results,
|
||||
store_backtest_stats)
|
||||
from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
|
@ -84,6 +85,8 @@ class Backtesting:
|
|||
self.strategylist: List[IStrategy] = []
|
||||
self.all_results: Dict[str, Dict] = {}
|
||||
self.processed_dfs: Dict[str, Dict] = {}
|
||||
self.rejected_dict: Dict[str, List] = {}
|
||||
self.rejected_df: Dict[str, Dict] = {}
|
||||
|
||||
self._exchange_name = self.config['exchange']['name']
|
||||
self.exchange = ExchangeResolver.load_exchange(
|
||||
|
@ -1056,6 +1059,18 @@ class Backtesting:
|
|||
return None
|
||||
return row
|
||||
|
||||
def _collate_rejected(self, pair, row):
|
||||
"""
|
||||
Temporarily store rejected signal information for downstream use in backtesting_analysis
|
||||
"""
|
||||
# It could be fun to enable hyperopt mode to write
|
||||
# a loss function to reduce rejected signals
|
||||
if (self.config.get('export', 'none') == 'signals' and
|
||||
self.dataprovider.runmode == RunMode.BACKTEST):
|
||||
if pair not in self.rejected_dict:
|
||||
self.rejected_dict[pair] = []
|
||||
self.rejected_dict[pair].append([row[DATE_IDX], row[ENTER_TAG_IDX]])
|
||||
|
||||
def backtest_loop(
|
||||
self, row: Tuple, pair: str, current_time: datetime, end_date: datetime,
|
||||
open_trade_count_start: int, trade_dir: Optional[LongShort],
|
||||
|
@ -1081,20 +1096,22 @@ class Backtesting:
|
|||
if (
|
||||
(self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0)
|
||||
and is_first
|
||||
and self.trade_slot_available(open_trade_count_start)
|
||||
and current_time != end_date
|
||||
and trade_dir is not None
|
||||
and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir)
|
||||
):
|
||||
trade = self._enter_trade(pair, row, trade_dir)
|
||||
if trade:
|
||||
# TODO: hacky workaround to avoid opening > max_open_trades
|
||||
# This emulates previous behavior - not sure if this is correct
|
||||
# Prevents entering if the trade-slot was freed in this candle
|
||||
open_trade_count_start += 1
|
||||
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
|
||||
LocalTrade.add_bt_trade(trade)
|
||||
self.wallets.update()
|
||||
if (self.trade_slot_available(open_trade_count_start)):
|
||||
trade = self._enter_trade(pair, row, trade_dir)
|
||||
if trade:
|
||||
# TODO: hacky workaround to avoid opening > max_open_trades
|
||||
# This emulates previous behavior - not sure if this is correct
|
||||
# Prevents entering if the trade-slot was freed in this candle
|
||||
open_trade_count_start += 1
|
||||
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
|
||||
LocalTrade.add_bt_trade(trade)
|
||||
self.wallets.update()
|
||||
else:
|
||||
self._collate_rejected(pair, row)
|
||||
|
||||
for trade in list(LocalTrade.bt_trades_open_pp[pair]):
|
||||
# 3. Process entry orders.
|
||||
|
@ -1236,8 +1253,8 @@ class Backtesting:
|
|||
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, DataFrame],
|
||||
timerange: TimeRange):
|
||||
self.progress.init_step(BacktestState.ANALYZE, 0)
|
||||
|
||||
logger.info(f"Running backtesting for Strategy {strat.get_strategy_name()}")
|
||||
strategy_name = strat.get_strategy_name()
|
||||
logger.info(f"Running backtesting for Strategy {strategy_name}")
|
||||
backtest_start_time = datetime.now(timezone.utc)
|
||||
self._set_strategy(strat)
|
||||
|
||||
|
@ -1272,37 +1289,21 @@ class Backtesting:
|
|||
)
|
||||
backtest_end_time = datetime.now(timezone.utc)
|
||||
results.update({
|
||||
'run_id': self.run_ids.get(strat.get_strategy_name(), ''),
|
||||
'run_id': self.run_ids.get(strategy_name, ''),
|
||||
'backtest_start_time': int(backtest_start_time.timestamp()),
|
||||
'backtest_end_time': int(backtest_end_time.timestamp()),
|
||||
})
|
||||
self.all_results[self.strategy.get_strategy_name()] = results
|
||||
self.all_results[strategy_name] = results
|
||||
|
||||
if (self.config.get('export', 'none') == 'signals' and
|
||||
self.dataprovider.runmode == RunMode.BACKTEST):
|
||||
self._generate_trade_signal_candles(preprocessed_tmp, results)
|
||||
self.processed_dfs[strategy_name] = generate_trade_signal_candles(
|
||||
preprocessed_tmp, results)
|
||||
self.rejected_df[strategy_name] = generate_rejected_signals(
|
||||
preprocessed_tmp, self.rejected_dict)
|
||||
|
||||
return min_date, max_date
|
||||
|
||||
def _generate_trade_signal_candles(self, preprocessed_df, bt_results):
|
||||
signal_candles_only = {}
|
||||
for pair in preprocessed_df.keys():
|
||||
signal_candles_only_df = DataFrame()
|
||||
|
||||
pairdf = preprocessed_df[pair]
|
||||
resdf = bt_results['results']
|
||||
pairresults = resdf.loc[(resdf["pair"] == pair)]
|
||||
|
||||
if pairdf.shape[0] > 0:
|
||||
for t, v in pairresults.open_date.items():
|
||||
allinds = pairdf.loc[(pairdf['date'] < v)]
|
||||
signal_inds = allinds.iloc[[-1]]
|
||||
signal_candles_only_df = pd.concat([signal_candles_only_df, signal_inds])
|
||||
|
||||
signal_candles_only[pair] = signal_candles_only_df
|
||||
|
||||
self.processed_dfs[self.strategy.get_strategy_name()] = signal_candles_only
|
||||
|
||||
def _get_min_cached_backtest_date(self):
|
||||
min_backtest_date = None
|
||||
backtest_cache_age = self.config.get('backtest_cache', constants.BACKTEST_CACHE_DEFAULT)
|
||||
|
@ -1365,8 +1366,9 @@ class Backtesting:
|
|||
|
||||
if (self.config.get('export', 'none') == 'signals' and
|
||||
self.dataprovider.runmode == RunMode.BACKTEST):
|
||||
store_backtest_signal_candles(
|
||||
self.config['exportfilename'], self.processed_dfs, dt_appendix)
|
||||
store_backtest_analysis_results(
|
||||
self.config['exportfilename'], self.processed_dfs, self.rejected_df,
|
||||
dt_appendix)
|
||||
|
||||
# Results may be mixed up now. Sort them so they follow --strategy-list order.
|
||||
if 'strategy_list' in self.config and len(self.results) > 0:
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -4,11 +4,11 @@ from datetime import datetime, timedelta, timezone
|
|||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from pandas import DataFrame, to_datetime
|
||||
from pandas import DataFrame, concat, 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)
|
||||
|
@ -46,29 +46,80 @@ def store_backtest_stats(
|
|||
file_dump_json(latest_filename, {'latest_backtest': str(filename.name)})
|
||||
|
||||
|
||||
def store_backtest_signal_candles(
|
||||
recordfilename: Path, candles: Dict[str, Dict], dtappendix: str) -> Path:
|
||||
def _store_backtest_analysis_data(
|
||||
recordfilename: Path, data: Dict[str, Dict],
|
||||
dtappendix: str, name: str) -> Path:
|
||||
"""
|
||||
Stores backtest trade signal candles
|
||||
Stores backtest trade candles for analysis
|
||||
:param recordfilename: Path object, which can either be a filename or a directory.
|
||||
Filenames will be appended with a timestamp right before the suffix
|
||||
while for directories, <directory>/backtest-result-<datetime>_signals.pkl will be used
|
||||
while for directories, <directory>/backtest-result-<datetime>_<name>.pkl will be used
|
||||
as filename
|
||||
:param stats: Dict containing the backtesting signal candles
|
||||
:param candles: Dict containing the backtesting data for analysis
|
||||
:param dtappendix: Datetime to use for the filename
|
||||
:param name: Name to use for the file, e.g. signals, rejected
|
||||
"""
|
||||
if recordfilename.is_dir():
|
||||
filename = (recordfilename / f'backtest-result-{dtappendix}_signals.pkl')
|
||||
filename = (recordfilename / f'backtest-result-{dtappendix}_{name}.pkl')
|
||||
else:
|
||||
filename = Path.joinpath(
|
||||
recordfilename.parent, f'{recordfilename.stem}-{dtappendix}_signals.pkl'
|
||||
recordfilename.parent, f'{recordfilename.stem}-{dtappendix}_{name}.pkl'
|
||||
)
|
||||
|
||||
file_dump_joblib(filename, candles)
|
||||
file_dump_joblib(filename, data)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def store_backtest_analysis_results(
|
||||
recordfilename: Path, candles: Dict[str, Dict], trades: Dict[str, Dict],
|
||||
dtappendix: str) -> None:
|
||||
_store_backtest_analysis_data(recordfilename, candles, dtappendix, "signals")
|
||||
_store_backtest_analysis_data(recordfilename, trades, dtappendix, "rejected")
|
||||
|
||||
|
||||
def generate_trade_signal_candles(preprocessed_df: Dict[str, DataFrame],
|
||||
bt_results: Dict[str, Any]) -> DataFrame:
|
||||
signal_candles_only = {}
|
||||
for pair in preprocessed_df.keys():
|
||||
signal_candles_only_df = DataFrame()
|
||||
|
||||
pairdf = preprocessed_df[pair]
|
||||
resdf = bt_results['results']
|
||||
pairresults = resdf.loc[(resdf["pair"] == pair)]
|
||||
|
||||
if pairdf.shape[0] > 0:
|
||||
for t, v in pairresults.open_date.items():
|
||||
allinds = pairdf.loc[(pairdf['date'] < v)]
|
||||
signal_inds = allinds.iloc[[-1]]
|
||||
signal_candles_only_df = concat([
|
||||
signal_candles_only_df.infer_objects(),
|
||||
signal_inds.infer_objects()])
|
||||
|
||||
signal_candles_only[pair] = signal_candles_only_df
|
||||
return signal_candles_only
|
||||
|
||||
|
||||
def generate_rejected_signals(preprocessed_df: Dict[str, DataFrame],
|
||||
rejected_dict: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
||||
rejected_candles_only = {}
|
||||
for pair, signals in rejected_dict.items():
|
||||
rejected_signals_only_df = DataFrame()
|
||||
pairdf = preprocessed_df[pair]
|
||||
|
||||
for t in signals:
|
||||
data_df_row = pairdf.loc[(pairdf['date'] == t[0])].copy()
|
||||
data_df_row['pair'] = pair
|
||||
data_df_row['enter_tag'] = t[1]
|
||||
|
||||
rejected_signals_only_df = concat([
|
||||
rejected_signals_only_df.infer_objects(),
|
||||
data_df_row.infer_objects()])
|
||||
|
||||
rejected_candles_only[pair] = rejected_signals_only_df
|
||||
return rejected_candles_only
|
||||
|
||||
|
||||
def _get_line_floatfmt(stake_currency: str) -> List[str]:
|
||||
"""
|
||||
Generate floatformat (goes in line with _generate_result_line())
|
||||
|
@ -273,7 +324,8 @@ def _get_resample_from_period(period: str) -> str:
|
|||
if period == 'day':
|
||||
return '1d'
|
||||
if period == 'week':
|
||||
return '1w'
|
||||
# Weekly defaulting to Monday.
|
||||
return '1W-MON'
|
||||
if period == 'month':
|
||||
return '1M'
|
||||
raise ValueError(f"Period {period} is not supported.")
|
||||
|
@ -295,6 +347,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,
|
||||
|
@ -304,6 +357,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:
|
||||
|
@ -380,7 +440,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
|
||||
|
@ -415,6 +476,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'],
|
||||
|
@ -433,7 +499,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']]),
|
||||
|
@ -498,6 +563,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
|
||||
}
|
||||
|
@ -890,8 +956,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()
|
||||
|
@ -246,14 +247,17 @@ def pair_candles(
|
|||
|
||||
@router.get('/pair_history', response_model=PairHistory, tags=['candle data'])
|
||||
def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
|
||||
freqaimodel: Optional[str] = None,
|
||||
config=Depends(get_config), exchange=Depends(get_exchange)):
|
||||
# The initial call to this endpoint can be slow, as it may need to initialize
|
||||
# the exchange class.
|
||||
config = deepcopy(config)
|
||||
config.update({
|
||||
'strategy': strategy,
|
||||
'timerange': timerange,
|
||||
'freqaimodel': freqaimodel if freqaimodel else config.get('freqaimodel'),
|
||||
})
|
||||
return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange, exchange)
|
||||
return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange)
|
||||
|
||||
|
||||
@router.get('/plot_config', response_model=PlotConfig, tags=['candle data'])
|
||||
|
@ -303,11 +307,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
|
||||
|
@ -419,16 +420,15 @@ class RPC:
|
|||
else:
|
||||
return 'draws'
|
||||
trades = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False)
|
||||
# Sell reason
|
||||
# Duration
|
||||
dur: Dict[str, List[float]] = {'wins': [], 'draws': [], 'losses': []}
|
||||
# Exit reason
|
||||
exit_reasons = {}
|
||||
for trade in trades:
|
||||
if trade.exit_reason not in exit_reasons:
|
||||
exit_reasons[trade.exit_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
|
||||
exit_reasons[trade.exit_reason][trade_win_loss(trade)] += 1
|
||||
|
||||
# Duration
|
||||
dur: Dict[str, List[float]] = {'wins': [], 'draws': [], 'losses': []}
|
||||
for trade in trades:
|
||||
if trade.close_date is not None and trade.open_date is not None:
|
||||
trade_dur = (trade.close_date - trade.open_date).total_seconds()
|
||||
dur[trade_win_loss(trade)].append(trade_dur)
|
||||
|
@ -581,15 +581,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 +629,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 +673,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,
|
||||
|
@ -1179,8 +1215,8 @@ class RPC:
|
|||
|
||||
@staticmethod
|
||||
def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str,
|
||||
timerange: str, exchange) -> Dict[str, Any]:
|
||||
timerange_parsed = TimeRange.parse_timerange(timerange)
|
||||
exchange) -> Dict[str, Any]:
|
||||
timerange_parsed = TimeRange.parse_timerange(config.get('timerange'))
|
||||
|
||||
_data = load_data(
|
||||
datadir=config["datadir"],
|
||||
|
@ -1191,7 +1227,8 @@ class RPC:
|
|||
candle_type=config.get('candle_type_def', CandleType.SPOT)
|
||||
)
|
||||
if pair not in _data:
|
||||
raise RPCException(f"No data for {pair}, {timeframe} in {timerange} found.")
|
||||
raise RPCException(
|
||||
f"No data for {pair}, {timeframe} in {config.get('timerange')} found.")
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
strategy = StrategyResolver.load_strategy(config)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[build-system]
|
||||
requires = ["setuptools >= 46.4.0", "wheel"]
|
||||
requires = ["setuptools >= 64.0.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.black]
|
||||
|
@ -68,6 +68,9 @@ target-version = "py38"
|
|||
extend-select = [
|
||||
"C90", # mccabe
|
||||
# "N", # pep8-naming
|
||||
"F", # pyflakes
|
||||
"E", # pycodestyle
|
||||
"W", # pycodestyle
|
||||
"UP", # pyupgrade
|
||||
"TID", # flake8-tidy-imports
|
||||
# "EXE", # flake8-executable
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
-r docs/requirements-docs.txt
|
||||
|
||||
coveralls==3.3.1
|
||||
ruff==0.0.261
|
||||
ruff==0.0.265
|
||||
mypy==1.2.0
|
||||
pre-commit==3.2.2
|
||||
pytest==7.3.0
|
||||
pre-commit==3.3.1
|
||||
pytest==7.3.1
|
||||
pytest-asyncio==0.21.0
|
||||
pytest-cov==4.0.0
|
||||
pytest-mock==3.10.0
|
||||
|
@ -18,15 +18,13 @@ pytest-random-order==1.1.0
|
|||
isort==5.12.0
|
||||
# For datetime mocking
|
||||
time-machine==2.9.0
|
||||
# fastapi testing
|
||||
httpx==0.23.3
|
||||
|
||||
# Convert jupyter notebooks to markdown documents
|
||||
nbconvert==7.3.1
|
||||
nbconvert==7.4.0
|
||||
|
||||
# mypy types
|
||||
types-cachetools==5.3.0.5
|
||||
types-filelock==3.2.7
|
||||
types-requests==2.28.11.17
|
||||
types-requests==2.30.0.0
|
||||
types-tabulate==0.9.0.2
|
||||
types-python-dateutil==2.8.19.12
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
-r requirements-freqai.txt
|
||||
|
||||
# Required for freqai-rl
|
||||
torch==1.13.1; python_version < '3.11'
|
||||
stable-baselines3==1.7.0; python_version < '3.11'
|
||||
sb3-contrib==1.7.0; python_version < '3.11'
|
||||
# Gym is forced to this version by stable-baselines3.
|
||||
setuptools==65.5.1 # Should be removed when gym is fixed.
|
||||
gym==0.21; python_version < '3.11'
|
||||
torch==2.0.0
|
||||
#until these branches will be released we can use this
|
||||
gymnasium==0.28.1
|
||||
stable_baselines3==2.0.0a5
|
||||
sb3_contrib>=2.0.0a4
|
||||
# Progress bar for stable-baselines3 and sb3-contrib
|
||||
tqdm==4.65.0
|
||||
|
|
|
@ -8,4 +8,4 @@ joblib==1.2.0
|
|||
catboost==1.1.1; platform_machine != 'aarch64' and 'arm' not in platform_machine and python_version < '3.11'
|
||||
lightgbm==3.3.5
|
||||
xgboost==1.7.5
|
||||
tensorboard==2.12.1
|
||||
tensorboard==2.13.0
|
||||
|
|
|
@ -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,16 +1,18 @@
|
|||
numpy==1.24.2
|
||||
pandas==1.5.3
|
||||
numpy==1.24.3
|
||||
pandas==2.0.1
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==3.0.59
|
||||
cryptography==40.0.1
|
||||
ccxt==3.0.97
|
||||
cryptography==40.0.2
|
||||
aiohttp==3.8.4
|
||||
SQLAlchemy==2.0.9
|
||||
python-telegram-bot==13.15
|
||||
SQLAlchemy==2.0.12
|
||||
python-telegram-bot==20.3
|
||||
# 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
|
||||
urllib3==1.26.15
|
||||
requests==2.30.0
|
||||
urllib3==2.0.2
|
||||
jsonschema==4.17.3
|
||||
TA-Lib==0.4.26
|
||||
technical==1.4.0
|
||||
|
@ -20,7 +22,7 @@ jinja2==3.1.2
|
|||
tables==3.8.0
|
||||
blosc==1.11.1
|
||||
joblib==1.2.0
|
||||
rich==13.3.3
|
||||
rich==13.3.5
|
||||
pyarrow==11.0.0; platform_machine != 'armv7l'
|
||||
|
||||
# find first, C search in arrays
|
||||
|
@ -29,18 +31,18 @@ py_find_1st==1.1.5
|
|||
# Load ticker files 30% faster
|
||||
python-rapidjson==1.10
|
||||
# Properly format api responses
|
||||
orjson==3.8.10
|
||||
orjson==3.8.12
|
||||
|
||||
# Notify systemd
|
||||
sdnotify==0.3.2
|
||||
|
||||
# API Server
|
||||
fastapi==0.95.0
|
||||
fastapi==0.95.1
|
||||
pydantic==1.10.7
|
||||
uvicorn==0.21.1
|
||||
uvicorn==0.22.0
|
||||
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,8 @@ python-dateutil==2.8.2
|
|||
schedule==1.2.0
|
||||
|
||||
#WS Messages
|
||||
websockets==11.0.1
|
||||
websockets==11.0.3
|
||||
janus==1.0.0
|
||||
|
||||
ast-comments==1.0.1
|
||||
packaging==23.1
|
||||
|
|
|
@ -348,12 +348,13 @@ class FtRestClient():
|
|||
params['limit'] = limit
|
||||
return self._get("pair_candles", params=params)
|
||||
|
||||
def pair_history(self, pair, timeframe, strategy, timerange=None):
|
||||
def pair_history(self, pair, timeframe, strategy, timerange=None, freqaimodel=None):
|
||||
"""Return historic, analyzed dataframe
|
||||
|
||||
:param pair: Pair to get data for
|
||||
:param timeframe: Only pairs with this timeframe available.
|
||||
:param strategy: Strategy to analyze and get values for
|
||||
:param freqaimodel: FreqAI model to use for analysis
|
||||
:param timerange: Timerange to get data for (same format than --timerange endpoints)
|
||||
:return: json object
|
||||
"""
|
||||
|
@ -361,6 +362,7 @@ class FtRestClient():
|
|||
"pair": pair,
|
||||
"timeframe": timeframe,
|
||||
"strategy": strategy,
|
||||
"freqaimodel": freqaimodel,
|
||||
"timerange": timerange if timerange else '',
|
||||
})
|
||||
|
||||
|
|
26
setup.py
26
setup.py
|
@ -12,16 +12,19 @@ hyperopt = [
|
|||
|
||||
freqai = [
|
||||
'scikit-learn',
|
||||
'joblib',
|
||||
'catboost; platform_machine != "aarch64"',
|
||||
'lightgbm',
|
||||
'xgboost'
|
||||
'xgboost',
|
||||
'tensorboard'
|
||||
]
|
||||
|
||||
freqai_rl = [
|
||||
'torch',
|
||||
'gymnasium',
|
||||
'stable-baselines3',
|
||||
'gym==0.21',
|
||||
'sb3-contrib'
|
||||
'sb3-contrib',
|
||||
'tqdm'
|
||||
]
|
||||
|
||||
hdf5 = [
|
||||
|
@ -32,11 +35,20 @@ hdf5 = [
|
|||
develop = [
|
||||
'coveralls',
|
||||
'mypy',
|
||||
'ruff',
|
||||
'pre-commit',
|
||||
'pytest',
|
||||
'pytest-asyncio',
|
||||
'pytest-cov',
|
||||
'pytest-mock',
|
||||
'pytest-random-order',
|
||||
'isort',
|
||||
'time-machine',
|
||||
'types-cachetools',
|
||||
'types-filelock',
|
||||
'types-requests',
|
||||
'types-tabulate',
|
||||
'types-python-dateutil'
|
||||
]
|
||||
|
||||
jupyter = [
|
||||
|
@ -91,7 +103,13 @@ setup(
|
|||
'aiofiles',
|
||||
'schedule',
|
||||
'websockets',
|
||||
'janus'
|
||||
'janus',
|
||||
'ast-comments',
|
||||
'aiohttp',
|
||||
'cryptography',
|
||||
'httpx',
|
||||
'python-dateutil',
|
||||
'packaging',
|
||||
],
|
||||
extras_require={
|
||||
'dev': all_extra,
|
||||
|
|
3
setup.sh
3
setup.sh
|
@ -49,8 +49,7 @@ function updateenv() {
|
|||
source .env/bin/activate
|
||||
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 wheel setuptools==65.5.1
|
||||
${PYTHON} -m pip install --upgrade pip wheel setuptools
|
||||
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
|
||||
|
@ -504,7 +503,7 @@ def get_default_conf(testdatadir):
|
|||
{"method": "StaticPairList"}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": True,
|
||||
"enabled": False,
|
||||
"token": "token",
|
||||
"chat_id": "0",
|
||||
"notification_settings": {},
|
||||
|
@ -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)
|
||||
|
|
|
@ -200,8 +200,17 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp
|
|||
assert 'trailing_stop_loss' in captured.out
|
||||
|
||||
# test date filtering
|
||||
args = get_args(base_args + ['--timerange', "20180129-20180130"])
|
||||
args = get_args(base_args +
|
||||
['--analysis-groups', "0", "1", "2",
|
||||
'--timerange', "20180129-20180130"]
|
||||
)
|
||||
start_analysis_entries_exits(args)
|
||||
captured = capsys.readouterr()
|
||||
assert 'enter_tag_long_a' in captured.out
|
||||
assert 'enter_tag_long_b' not in captured.out
|
||||
|
||||
# Due to the backtest mock, there's no rejected signals generated.
|
||||
args = get_args(base_args + ['--rejected-signals'])
|
||||
start_analysis_entries_exits(args)
|
||||
captured = capsys.readouterr()
|
||||
assert 'no rejected signals' in captured.out
|
||||
|
|
|
@ -528,9 +528,11 @@ class TestCCXTExchange():
|
|||
assert res[1] == timeframe
|
||||
assert res[2] == candle_type
|
||||
candles = res[3]
|
||||
candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * 0.9
|
||||
candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms
|
||||
assert len(candles) >= min(candle_count, candle_count1)
|
||||
factor = 0.9
|
||||
candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor
|
||||
candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor
|
||||
assert len(candles) >= min(candle_count, candle_count1), \
|
||||
f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {factor}"
|
||||
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
|
||||
|
||||
def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
|
|
|
@ -4932,7 +4932,7 @@ def test_get_maintenance_ratio_and_amt_exceptions(mocker, default_conf, leverage
|
|||
|
||||
exchange._leverage_tiers = leverage_tiers
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
DependencyException,
|
||||
match='nominal value can not be lower than 0',
|
||||
):
|
||||
exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT:USDT', -1)
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
from unittest.mock import AsyncMock, MagicMock, PropertyMock
|
||||
|
||||
import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||
from freqtrade.exceptions import RetryableOrderError
|
||||
from freqtrade.exceptions import RetryableOrderError, TemporaryError
|
||||
from freqtrade.exchange.exchange import timeframe_to_minutes
|
||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has
|
||||
from tests.conftest import EXMS, get_patched_exchange, log_has
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
|
||||
|
@ -278,7 +278,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog,
|
|||
'fetchLeverageTiers': False,
|
||||
'fetchMarketLeverageTiers': True,
|
||||
})
|
||||
api_mock.fetch_market_leverage_tiers = get_mock_coro(side_effect=[
|
||||
api_mock.fetch_market_leverage_tiers = AsyncMock(side_effect=[
|
||||
[
|
||||
{
|
||||
'tier': 1,
|
||||
|
@ -341,6 +341,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog,
|
|||
}
|
||||
},
|
||||
],
|
||||
TemporaryError("this Failed"),
|
||||
[
|
||||
{
|
||||
'tier': 1,
|
||||
|
|
|
@ -41,7 +41,7 @@ def can_run_model(model: str) -> None:
|
|||
if is_pytorch_model and is_mac() and not is_arm():
|
||||
pytest.skip("Reinforcement learning / PyTorch module not available on intel based Mac OS.")
|
||||
|
||||
if is_pytorch_model and is_py11():
|
||||
if is_pytorch_model:
|
||||
pytest.skip("Reinforcement learning / PyTorch currently not available on python 3.11.")
|
||||
|
||||
|
||||
|
@ -50,7 +50,8 @@ def can_run_model(model: str) -> None:
|
|||
('XGBoostRegressor', False, True, False, True, False, 10),
|
||||
('XGBoostRFRegressor', False, False, False, True, False, 0),
|
||||
('CatboostRegressor', False, False, False, True, True, 0),
|
||||
('PyTorchMLPRegressor', False, False, False, True, False, 0),
|
||||
('PyTorchMLPRegressor', False, False, False, False, False, 0),
|
||||
('PyTorchTransformerRegressor', False, False, False, False, False, 0),
|
||||
('ReinforcementLearner', False, True, False, True, False, 0),
|
||||
('ReinforcementLearner_multiproc', False, False, False, True, False, 0),
|
||||
('ReinforcementLearner_test_3ac', False, False, False, False, False, 0),
|
||||
|
@ -82,10 +83,13 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
|
|||
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
|
||||
freqai_conf["freqai"]["rl_config"]["drop_ohlc_from_features"] = True
|
||||
|
||||
if 'PyTorchMLPRegressor' in model:
|
||||
if 'PyTorch' in model:
|
||||
model_save_ext = 'zip'
|
||||
pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters()
|
||||
freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp)
|
||||
if 'Transformer' in model:
|
||||
# transformer model takes a window, unlike the MLP regressor
|
||||
freqai_conf.update({"conv_width": 10})
|
||||
|
||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||
|
@ -228,6 +232,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
|
|||
("XGBoostRegressor", 2, "freqai_test_strat"),
|
||||
("CatboostRegressor", 2, "freqai_test_strat"),
|
||||
("PyTorchMLPRegressor", 2, "freqai_test_strat"),
|
||||
("PyTorchTransformerRegressor", 2, "freqai_test_strat"),
|
||||
("ReinforcementLearner", 3, "freqai_rl_test_strat"),
|
||||
("XGBoostClassifier", 2, "freqai_test_classifier"),
|
||||
("LightGBMClassifier", 2, "freqai_test_classifier"),
|
||||
|
@ -253,9 +258,12 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog)
|
|||
if 'test_4ac' in model:
|
||||
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
|
||||
|
||||
if 'PyTorchMLP' in model:
|
||||
if 'PyTorch' in model:
|
||||
pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters()
|
||||
freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp)
|
||||
if 'Transformer' in model:
|
||||
# transformer model takes a window, unlike the MLP regressor
|
||||
freqai_conf.update({"conv_width": 10})
|
||||
|
||||
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
|
||||
{"indicator_periods_candles": [2]})
|
||||
|
|
|
@ -354,7 +354,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
|||
mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats')
|
||||
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
|
||||
sbs = mocker.patch('freqtrade.optimize.backtesting.store_backtest_stats')
|
||||
sbc = mocker.patch('freqtrade.optimize.backtesting.store_backtest_signal_candles')
|
||||
sbc = mocker.patch('freqtrade.optimize.backtesting.store_backtest_analysis_results')
|
||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||
PropertyMock(return_value=['UNITTEST/BTC']))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
@ -21,7 +21,7 @@ from freqtrade.optimize.optimize_reports import (_get_resample_from_period, gene
|
|||
generate_periodic_breakdown_stats,
|
||||
generate_strategy_comparison,
|
||||
generate_trading_stats, show_sorted_pairlist,
|
||||
store_backtest_signal_candles,
|
||||
store_backtest_analysis_results,
|
||||
store_backtest_stats, text_table_bt_results,
|
||||
text_table_exit_reason, text_table_strategy)
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
|
@ -232,17 +232,17 @@ def test_store_backtest_candles(testdatadir, mocker):
|
|||
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
|
||||
|
||||
# mock directory exporting
|
||||
store_backtest_signal_candles(testdatadir, candle_dict, '2022_01_01_15_05_13')
|
||||
store_backtest_analysis_results(testdatadir, candle_dict, {}, '2022_01_01_15_05_13')
|
||||
|
||||
assert dump_mock.call_count == 1
|
||||
assert dump_mock.call_count == 2
|
||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||
assert str(dump_mock.call_args_list[0][0][0]).endswith('_signals.pkl')
|
||||
|
||||
dump_mock.reset_mock()
|
||||
# mock file exporting
|
||||
filename = Path(testdatadir / 'testresult')
|
||||
store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13')
|
||||
assert dump_mock.call_count == 1
|
||||
store_backtest_analysis_results(filename, candle_dict, {}, '2022_01_01_15_05_13')
|
||||
assert dump_mock.call_count == 2
|
||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||
# result will be testdatadir / testresult-<timestamp>_signals.pkl
|
||||
assert str(dump_mock.call_args_list[0][0][0]).endswith('_signals.pkl')
|
||||
|
@ -254,10 +254,11 @@ def test_write_read_backtest_candles(tmpdir):
|
|||
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
|
||||
|
||||
# test directory exporting
|
||||
stored_file = store_backtest_signal_candles(Path(tmpdir), candle_dict, '2022_01_01_15_05_13')
|
||||
scp = stored_file.open("rb")
|
||||
pickled_signal_candles = joblib.load(scp)
|
||||
scp.close()
|
||||
sample_date = '2022_01_01_15_05_13'
|
||||
store_backtest_analysis_results(Path(tmpdir), candle_dict, {}, sample_date)
|
||||
stored_file = Path(tmpdir / f'backtest-result-{sample_date}_signals.pkl')
|
||||
with stored_file.open("rb") as scp:
|
||||
pickled_signal_candles = joblib.load(scp)
|
||||
|
||||
assert pickled_signal_candles.keys() == candle_dict.keys()
|
||||
assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys()
|
||||
|
@ -268,10 +269,10 @@ def test_write_read_backtest_candles(tmpdir):
|
|||
|
||||
# test file exporting
|
||||
filename = Path(tmpdir / 'testresult')
|
||||
stored_file = store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13')
|
||||
scp = stored_file.open("rb")
|
||||
pickled_signal_candles = joblib.load(scp)
|
||||
scp.close()
|
||||
store_backtest_analysis_results(filename, candle_dict, {}, sample_date)
|
||||
stored_file = Path(tmpdir / f'testresult-{sample_date}_signals.pkl')
|
||||
with stored_file.open("rb") as scp:
|
||||
pickled_signal_candles = joblib.load(scp)
|
||||
|
||||
assert pickled_signal_candles.keys() == candle_dict.keys()
|
||||
assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys()
|
||||
|
@ -465,11 +466,14 @@ def test_generate_periodic_breakdown_stats(testdatadir):
|
|||
def test__get_resample_from_period():
|
||||
|
||||
assert _get_resample_from_period('day') == '1d'
|
||||
assert _get_resample_from_period('week') == '1w'
|
||||
assert _get_resample_from_period('week') == '1W-MON'
|
||||
assert _get_resample_from_period('month') == '1M'
|
||||
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)))
|
||||
|
|
|
@ -28,6 +28,7 @@ def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
|
|||
|
||||
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
default_conf['telegram']['enabled'] = True
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
|
@ -52,6 +53,7 @@ def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
|||
|
||||
def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
default_conf['telegram']['enabled'] = True
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
||||
|
||||
|
@ -85,7 +87,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
|
|||
def test_send_msg_telegram_error(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', side_effect=ValueError())
|
||||
|
||||
default_conf['telegram']['enabled'] = True
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc_manager = RPCManager(freqtradebot)
|
||||
rpc_manager.send_msg({
|
||||
|
@ -99,6 +101,7 @@ def test_send_msg_telegram_error(mocker, default_conf, caplog) -> None:
|
|||
|
||||
def test_process_msg_queue(mocker, default_conf, caplog) -> None:
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg')
|
||||
default_conf['telegram']['enabled'] = True
|
||||
default_conf['telegram']['allow_custom_messages'] = True
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
|
||||
|
||||
|
@ -115,9 +118,9 @@ def test_process_msg_queue(mocker, default_conf, caplog) -> None:
|
|||
|
||||
|
||||
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
default_conf['telegram']['enabled'] = True
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg')
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc_manager = RPCManager(freqtradebot)
|
||||
rpc_manager.send_msg({
|
||||
|
@ -166,7 +169,8 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non
|
|||
caplog)
|
||||
|
||||
|
||||
def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
def test_startupmessages_telegram_enabled(mocker, default_conf) -> None:
|
||||
default_conf['telegram']['enabled'] = True
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
|
||||
|
|
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