Merge pull request #8224 from freqtrade/new_release

New release 2023.2
This commit is contained in:
Matthias 2023-02-26 14:53:52 +01:00 committed by GitHub
commit a31045874e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
126 changed files with 5099 additions and 2336 deletions

View File

@ -23,7 +23,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ ubuntu-18.04, ubuntu-20.04, ubuntu-22.04 ] os: [ ubuntu-20.04, ubuntu-22.04 ]
python-version: ["3.8", "3.9", "3.10"] python-version: ["3.8", "3.9", "3.10"]
steps: steps:

View File

@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/pycqa/flake8 - repo: https://github.com/pycqa/flake8
rev: "4.0.1" rev: "6.0.0"
hooks: hooks:
- id: flake8 - id: flake8
# stages: [push] # stages: [push]
@ -13,22 +13,22 @@ repos:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
additional_dependencies: additional_dependencies:
- types-cachetools==5.2.1 - types-cachetools==5.3.0.0
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.28.11.8 - types-requests==2.28.11.13
- types-tabulate==0.9.0.0 - types-tabulate==0.9.0.0
- types-python-dateutil==2.8.19.6 - types-python-dateutil==2.8.19.6
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
rev: "5.10.1" rev: "5.12.0"
hooks: hooks:
- id: isort - id: isort
name: isort (python) name: isort (python)
# stages: [push] # stages: [push]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0 rev: v4.4.0
hooks: hooks:
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: | exclude: |

View File

@ -1,4 +1,4 @@
FROM python:3.10.7-slim-bullseye as base FROM python:3.10.10-slim-bullseye as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8

View File

@ -40,6 +40,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
- [X] [Binance](https://www.binance.com/) - [X] [Binance](https://www.binance.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [OKX](https://okx.com/) - [X] [OKX](https://okx.com/)
- [X] [Bybit](https://bybit.com/)
Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in. Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in.
@ -164,6 +165,10 @@ first. If it hasn't been reported, please
ensure you follow the template guide so that the team can assist you as ensure you follow the template guide so that the team can assist you as
quickly as possible. quickly as possible.
For every [issue](https://github.com/freqtrade/freqtrade/issues/new/choose) created, kindly follow up and mark satisfaction or reminder to close issue when equilibrium ground is reached.
--Maintain github's [community policy](https://docs.github.com/en/site-policy/github-terms/github-community-code-of-conduct)--
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement) ### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
Have you a great idea to improve the bot you want to share? Please, Have you a great idea to improve the bot you want to share? Please,

Binary file not shown.

View File

@ -14,5 +14,8 @@ if ($pyv -eq '3.9') {
if ($pyv -eq '3.10') { if ($pyv -eq '3.10') {
pip install build_helpers\TA_Lib-0.4.25-cp310-cp310-win_amd64.whl pip install build_helpers\TA_Lib-0.4.25-cp310-cp310-win_amd64.whl
} }
if ($pyv -eq '3.11') {
pip install build_helpers\TA_Lib-0.4.25-cp311-cp311-win_amd64.whl
}
pip install -r requirements-dev.txt pip install -r requirements-dev.txt
pip install -e . pip install -e .

View File

@ -48,7 +48,7 @@
], ],
"freqai": { "freqai": {
"enabled": true, "enabled": true,
"purge_old_models": true, "purge_old_models": 2,
"train_period_days": 15, "train_period_days": 15,
"backtest_period_days": 7, "backtest_period_days": 7,
"live_retrain_hours": 0, "live_retrain_hours": 0,

View File

@ -60,6 +60,7 @@
"force_entry": "market", "force_entry": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_price_type": "last",
"stoploss_on_exchange_interval": 60, "stoploss_on_exchange_interval": 60,
"stoploss_on_exchange_limit_ratio": 0.99 "stoploss_on_exchange_limit_ratio": 0.99
}, },

View File

@ -1,4 +1,4 @@
FROM python:3.9.12-slim-bullseye as base FROM python:3.9.16-slim-bullseye as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8

View File

@ -192,7 +192,7 @@ $RepeatedMsgReduction on
### Logging to journald ### Logging to journald
This needs the `systemd` python package installed as the dependency, which is not available on Windows. Hence, the whole journald logging functionality is not available for a bot running on Windows. This needs the `cysystemd` python package installed as dependency (`pip install cysystemd`), which is not available on Windows. Hence, the whole journald logging functionality is not available for a bot running on Windows.
To send Freqtrade log messages to `journald` system service use the `--logfile` command line option with the value in the following format: To send Freqtrade log messages to `journald` system service use the `--logfile` command line option with the value in the following format:

View File

@ -666,7 +666,7 @@ You should also make sure to read the [Exchanges](exchanges.md) section of the d
### Using proxy with Freqtrade ### Using proxy with Freqtrade
To use a proxy with freqtrade, export your proxy settings using the variables `"HTTP_PROXY"` and `"HTTPS_PROXY"` set to the appropriate values. To use a proxy with freqtrade, export your proxy settings using the variables `"HTTP_PROXY"` and `"HTTPS_PROXY"` set to the appropriate values.
This will have the proxy settings applied to everything (telegram, coingecko, ...) except exchange requests. This will have the proxy settings applied to everything (telegram, coingecko, ...) **except** for exchange requests.
``` bash ``` bash
export HTTP_PROXY="http://addr:port" export HTTP_PROXY="http://addr:port"
@ -682,11 +682,12 @@ To use a proxy for exchange connections - you will have to define the proxies as
{ {
"exchange": { "exchange": {
"ccxt_config": { "ccxt_config": {
"aiohttp_proxy": "http://addr:port", "aiohttp_proxy": "http://addr:port",
"proxies": { "proxies": {
"http": "http://addr:port", "http": "http://addr:port",
"https": "http://addr:port" "https": "http://addr:port"
}, },
}
} }
} }
``` ```

View File

@ -363,7 +363,7 @@ from pathlib import Path
exchange = ccxt.binance({ exchange = ccxt.binance({
'apiKey': '<apikey>', 'apiKey': '<apikey>',
'secret': '<secret>' 'secret': '<secret>'
'options': {'defaultType': 'future'} 'options': {'defaultType': 'swap'}
}) })
_ = exchange.load_markets() _ = exchange.load_markets()

View File

@ -243,8 +243,8 @@ OKX requires a passphrase for each api key, you will therefore need to add this
OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode. OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode.
!!! Warning "Futures" !!! Warning "Futures"
OKX Futures has the concept of "position mode" - which can be Net or long/short (hedge mode). OKX Futures has the concept of "position mode" - which can be "Buy/Sell" or long/short (hedge mode).
Freqtrade supports both modes (we recommend to use net mode) - but changing the mode mid-trading is not supported and will lead to exceptions and failures to place trades. Freqtrade supports both modes (we recommend to use Buy/Sell mode) - but changing the mode mid-trading is not supported and will lead to exceptions and failures to place trades.
OKX also only provides MARK candles for the past ~3 months. Backtesting futures prior to that date will therefore lead to slight deviations, as funding-fees cannot be calculated correctly without this data. OKX also only provides MARK candles for the past ~3 months. Backtesting futures prior to that date will therefore lead to slight deviations, as funding-fees cannot be calculated correctly without this data.
## Gate.io ## Gate.io
@ -255,6 +255,18 @@ OKX requires a passphrase for each api key, you will therefore need to add this
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0). Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value. The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
## Bybit
Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode.
Users with unified accounts (there's no way back) can create a Sub-account which will start as "non-unified", and can therefore use isolated futures.
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors.
As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well.
!!! Tip "Stoploss on Exchange"
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
## All exchanges ## All exchanges
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.

View File

@ -2,7 +2,7 @@
## Supported Markets ## Supported Markets
Freqtrade supports spot trading only. Freqtrade supports spot trading, as well as (isolated) futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an uptodate list of supported exchanges.
### Can my bot open short positions? ### Can my bot open short positions?
@ -248,8 +248,26 @@ The Edge module is mostly a result of brainstorming of [@mishaker](https://githu
You can find further info on expectancy, win rate, risk management and position size in the following sources: You can find further info on expectancy, win rate, risk management and position size in the following sources:
- https://www.tradeciety.com/ultimate-math-guide-for-traders/ - https://www.tradeciety.com/ultimate-math-guide-for-traders/
- http://www.vantharp.com/tharp-concepts/expectancy.asp
- https://samuraitradingacademy.com/trading-expectancy/ - https://samuraitradingacademy.com/trading-expectancy/
- https://www.learningmarkets.com/determining-expectancy-in-your-trading/ - https://www.learningmarkets.com/determining-expectancy-in-your-trading/
- http://www.lonestocktrader.com/make-money-trading-positive-expectancy/ - https://www.lonestocktrader.com/make-money-trading-positive-expectancy/
- https://www.babypips.com/trading/trade-expectancy-matter - https://www.babypips.com/trading/trade-expectancy-matter
## Official channels
Freqtrade is using exclusively the following official channels:
* [Freqtrade discord server](https://discord.gg/p7nuUNVfP7)
* [Freqtrade documentation (https://freqtrade.io)](https://freqtrade.io)
* [Freqtrade github organization](https://github.com/freqtrade)
Nobody affiliated with the freqtrade project will ask you about your exchange keys or anything else exposing your funds to exploitation.
Should you be asked to expose your exchange keys or send funds to some random wallet, then please don't follow these instructions.
Failing to follow these guidelines will not be responsibility of freqtrade.
## "Freqtrade token"
Freqtrade does not have a Crypto token offering.
Token offerings you find on the internet referring Freqtrade, FreqAI or freqUI must be considered to be a scam, trying to exploit freqtrade's popularity for their own, nefarious gains.

View File

@ -9,7 +9,7 @@ FreqAI is configured through the typical [Freqtrade config file](configuration.m
```json ```json
"freqai": { "freqai": {
"enabled": true, "enabled": true,
"purge_old_models": true, "purge_old_models": 2,
"train_period_days": 30, "train_period_days": 30,
"backtest_period_days": 7, "backtest_period_days": 7,
"identifier" : "unique-id", "identifier" : "unique-id",
@ -165,10 +165,10 @@ Below are the values you can expect to include/use inside a typical strategy dat
## Setting the `startup_candle_count` ## Setting the `startup_candle_count`
The `startup_candle_count` in the FreqAI strategy needs to be set up in the same way as in the standard Freqtrade strategy (see details [here](strategy-customization.md#strategy-startup-period)). This value is used by Freqtrade to ensure that a sufficient amount of data is provided when calling the `dataprovider`, to avoid any NaNs at the beginning of the first training. You can easily set this value by identifying the longest period (in candle units) which is passed to the indicator creation functions (e.g., Ta-Lib functions). In the presented example, `startup_candle_count` is 20 since this is the maximum value in `indicators_periods_candles`. The `startup_candle_count` in the FreqAI strategy needs to be set up in the same way as in the standard Freqtrade strategy (see details [here](strategy-customization.md#strategy-startup-period)). This value is used by Freqtrade to ensure that a sufficient amount of data is provided when calling the `dataprovider`, to avoid any NaNs at the beginning of the first training. You can easily set this value by identifying the longest period (in candle units) which is passed to the indicator creation functions (e.g., TA-Lib functions). In the presented example, `startup_candle_count` is 20 since this is the maximum value in `indicators_periods_candles`.
!!! Note !!! Note
There are instances where the Ta-Lib functions actually require more data than just the passed `period` or else the feature dataset gets populated with NaNs. Anecdotally, multiplying the `startup_candle_count` by 2 always leads to a fully NaN free training dataset. Hence, it is typically safest to multiply the expected `startup_candle_count` by 2. Look out for this log message to confirm that the data is clean: There are instances where the TA-Lib functions actually require more data than just the passed `period` or else the feature dataset gets populated with NaNs. Anecdotally, multiplying the `startup_candle_count` by 2 always leads to a fully NaN free training dataset. Hence, it is typically safest to multiply the expected `startup_candle_count` by 2. Look out for this log message to confirm that the data is clean:
``` ```
2022-08-31 15:14:04 - freqtrade.freqai.data_kitchen - INFO - dropped 0 training points due to NaNs in populated dataset 4319. 2022-08-31 15:14:04 - freqtrade.freqai.data_kitchen - INFO - dropped 0 training points due to NaNs in populated dataset 4319.
@ -205,7 +205,7 @@ All of the aforementioned model libraries implement gradient boosted decision tr
* LightGBM: https://lightgbm.readthedocs.io/en/v3.3.2/# * LightGBM: https://lightgbm.readthedocs.io/en/v3.3.2/#
* XGBoost: https://xgboost.readthedocs.io/en/stable/# * XGBoost: https://xgboost.readthedocs.io/en/stable/#
There are also numerous online articles describing and comparing the algorithms. Some relatively light-weight examples would be [CatBoost vs. LightGBM vs. XGBoost — Which is the best algorithm?](https://towardsdatascience.com/catboost-vs-lightgbm-vs-xgboost-c80f40662924#:~:text=In%20CatBoost%2C%20symmetric%20trees%2C%20or,the%20same%20depth%20can%20differ.) and [XGBoost, LightGBM or CatBoost — which boosting algorithm should I use?](https://medium.com/riskified-technology/xgboost-lightgbm-or-catboost-which-boosting-algorithm-should-i-use-e7fda7bb36bc). Keep in mind that the performance of each model is highly dependent on the application and so any reported metrics might not be true for your particular use of the model. There are also numerous online articles describing and comparing the algorithms. Some relatively lightweight examples would be [CatBoost vs. LightGBM vs. XGBoost — Which is the best algorithm?](https://towardsdatascience.com/catboost-vs-lightgbm-vs-xgboost-c80f40662924#:~:text=In%20CatBoost%2C%20symmetric%20trees%2C%20or,the%20same%20depth%20can%20differ.) and [XGBoost, LightGBM or CatBoost — which boosting algorithm should I use?](https://medium.com/riskified-technology/xgboost-lightgbm-or-catboost-which-boosting-algorithm-should-i-use-e7fda7bb36bc). Keep in mind that the performance of each model is highly dependent on the application and so any reported metrics might not be true for your particular use of the model.
Apart from the models already available in FreqAI, it is also possible to customize and create your own prediction models using the `IFreqaiModel` class. You are encouraged to inherit `fit()`, `train()`, and `predict()` to customize various aspects of the training procedures. You can place custom FreqAI models in `user_data/freqaimodels` - and freqtrade will pick them up from there based on the provided `--freqaimodel` name - which has to correspond to the class name of your custom model. Apart from the models already available in FreqAI, it is also possible to customize and create your own prediction models using the `IFreqaiModel` class. You are encouraged to inherit `fit()`, `train()`, and `predict()` to customize various aspects of the training procedures. You can place custom FreqAI models in `user_data/freqaimodels` - and freqtrade will pick them up from there based on the provided `--freqaimodel` name - which has to correspond to the class name of your custom model.
Make sure to use unique names to avoid overriding built-in models. Make sure to use unique names to avoid overriding built-in models.

View File

@ -8,7 +8,7 @@ Low level feature engineering is performed in the user strategy within a set of
|---------------|-------------| |---------------|-------------|
| `feature_engineering__expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. | `feature_engineering__expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
| `feature_engineering__expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`. | `feature_engineering__expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g. day of the week). | `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week).
| `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals. | `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals.
Meanwhile, high level feature engineering is handled within `"feature_parameters":{}` in the FreqAI config. Within this file, it is possible to decide large scale feature expansions on top of the `base_features` such as "including correlated pairs" or "including informative timeframes" or even "including recent candles." Meanwhile, high level feature engineering is handled within `"feature_parameters":{}` in the FreqAI config. Within this file, it is possible to decide large scale feature expansions on top of the `base_features` such as "including correlated pairs" or "including informative timeframes" or even "including recent candles."
@ -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: 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 ```python
def feature_engineering_expand_all(self, dataframe, period, **kwargs): def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined This function will automatically expand the defined features on the config defined
@ -28,8 +28,13 @@ It is advisable to start from the template `feature_engineering_*` functions in
All features must be prepended with `%` to be recognized by FreqAI internals. All features must be prepended with `%` to be recognized by FreqAI internals.
Access metadata such as the current pair/timeframe/period with:
`metadata["pair"]` `metadata["tf"]` `metadata["period"]`
:param df: strategy dataframe which will receive the features :param df: strategy dataframe which will receive the features
:param period: period of the indicator - usage example: :param period: period of the indicator - usage example:
:param metadata: metadata of current pair
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
""" """
@ -62,7 +67,7 @@ It is advisable to start from the template `feature_engineering_*` functions in
return dataframe return dataframe
def feature_engineering_expand_basic(self, dataframe, **kwargs): def feature_engineering_expand_basic(self, dataframe, metadata, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined This function will automatically expand the defined features on the config defined
@ -75,9 +80,14 @@ It is advisable to start from the template `feature_engineering_*` functions in
Features defined here will *not* be automatically duplicated on user defined Features defined here will *not* be automatically duplicated on user defined
`indicator_periods_candles` `indicator_periods_candles`
Access metadata such as the current pair/timeframe with:
`metadata["pair"]` `metadata["tf"]`
All features must be prepended with `%` to be recognized by FreqAI internals. All features must be prepended with `%` to be recognized by FreqAI internals.
:param df: strategy dataframe which will receive the features :param df: strategy dataframe which will receive the features
:param metadata: metadata of current pair
dataframe["%-pct-change"] = dataframe["close"].pct_change() dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200) dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
""" """
@ -86,7 +96,7 @@ It is advisable to start from the template `feature_engineering_*` functions in
dataframe["%-raw_price"] = dataframe["close"] dataframe["%-raw_price"] = dataframe["close"]
return dataframe return dataframe
def feature_engineering_standard(self, dataframe, **kwargs): def feature_engineering_standard(self, dataframe, metadata, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe. This optional function will be called once with the dataframe of the base timeframe.
@ -98,22 +108,32 @@ It is advisable to start from the template `feature_engineering_*` functions in
This function is a good place for any feature that should not be auto-expanded upon This function is a good place for any feature that should not be auto-expanded upon
(e.g. day of the week). (e.g. day of the week).
Access metadata such as the current pair with:
`metadata["pair"]`
All features must be prepended with `%` to be recognized by FreqAI internals. All features must be prepended with `%` to be recognized by FreqAI internals.
:param df: strategy dataframe which will receive the features :param df: strategy dataframe which will receive the features
:param metadata: metadata of current pair
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7 usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
""" """
dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7 dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25 dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
return dataframe return dataframe
def set_freqai_targets(self, dataframe, **kwargs): def set_freqai_targets(self, dataframe, metadata, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
Required function to set the targets for the model. Required function to set the targets for the model.
All targets must be prepended with `&` to be recognized by the FreqAI internals. All targets must be prepended with `&` to be recognized by the FreqAI internals.
Access metadata such as the current pair with:
`metadata["pair"]`
:param df: strategy dataframe which will receive the targets :param df: strategy dataframe which will receive the targets
:param metadata: metadata of current pair
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"] usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
""" """
dataframe["&-s_close"] = ( dataframe["&-s_close"] = (
@ -161,6 +181,19 @@ 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` 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$. $= 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.
```py
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
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"`.
### Returning additional info from training ### Returning additional info from training
Important metrics can be returned to the strategy at the end of each model training by assigning them to `dk.data['extra_returns_per_train']['my_new_value'] = XYZ` inside the custom prediction model class. Important metrics can be returned to the strategy at the end of each model training by assigning them to `dk.data['extra_returns_per_train']['my_new_value'] = XYZ` inside the custom prediction model class.
@ -201,7 +234,7 @@ This will perform PCA on the features and reduce their dimensionality so that th
## Inlier metric ## Inlier metric
The `inlier_metric` is a metric aimed at quantifying how similar a the features of a data point are to the most recent historic data points. The `inlier_metric` is a metric aimed at quantifying how similar the features of a data point are to the most recent historical data points.
You define the lookback window by setting `inlier_metric_window` and FreqAI computes the distance between the present time point and each of the previous `inlier_metric_window` lookback points. A Weibull function is fit to each of the lookback distributions and its cumulative distribution function (CDF) is used to produce a quantile for each lookback point. The `inlier_metric` is then computed for each time point as the average of the corresponding lookback quantiles. The figure below explains the concept for an `inlier_metric_window` of 5. You define the lookback window by setting `inlier_metric_window` and FreqAI computes the distance between the present time point and each of the previous `inlier_metric_window` lookback points. A Weibull function is fit to each of the lookback distributions and its cumulative distribution function (CDF) is used to produce a quantile for each lookback point. The `inlier_metric` is then computed for each time point as the average of the corresponding lookback quantiles. The figure below explains the concept for an `inlier_metric_window` of 5.

View File

@ -15,10 +15,9 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `identifier` | **Required.** <br> A unique ID for the current model. If models are saved to disk, the `identifier` allows for reloading specific pre-trained models/data. <br> **Datatype:** String. | `identifier` | **Required.** <br> A unique ID for the current model. If models are saved to disk, the `identifier` allows for reloading specific pre-trained models/data. <br> **Datatype:** String.
| `live_retrain_hours` | Frequency of retraining during dry/live runs. <br> **Datatype:** Float > 0. <br> Default: `0` (models retrain as often as possible). | `live_retrain_hours` | Frequency of retraining during dry/live runs. <br> **Datatype:** Float > 0. <br> Default: `0` (models retrain as often as possible).
| `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old. <br> **Datatype:** Positive integer. <br> Default: `0` (models never expire). | `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old. <br> **Datatype:** Positive integer. <br> Default: `0` (models never expire).
| `purge_old_models` | Delete all unused models during live runs (not relevant to backtesting). If set to false (not default), dry/live runs will accumulate all unused models to disk. If <br> **Datatype:** Boolean. <br> Default: `True`. | `purge_old_models` | Number of models to keep on disk (not relevant to backtesting). Default is 2, which means that dry/live runs will keep the latest 2 models on disk. Setting to 0 keeps all models. This parameter also accepts a boolean to maintain backwards compatibility. <br> **Datatype:** Integer. <br> Default: `2`.
| `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`. <br> **Datatype:** Boolean. <br> Default: `False` (no models are saved). | `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`. <br> **Datatype:** Boolean. <br> Default: `False` (no models are saved).
| `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer. | `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer.
| `follow_mode` | Use a `follower` that will look for models associated with a specific `identifier` and load those for inferencing. A `follower` will **not** train new models. <br> **Datatype:** Boolean. <br> Default: `False`.
| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). <br> **Datatype:** Boolean. <br> Default: `False`. | `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). <br> **Datatype:** Boolean. <br> Default: `False`.
| `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False` | `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False`
| `data_kitchen_thread_count` | <br> Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.). This has no impact on the number of threads used for training. If user does not set it (default), FreqAI will use max number of threads - 2 (leaving 1 physical core available for Freqtrade bot and FreqUI) <br> **Datatype:** Positive integer. | `data_kitchen_thread_count` | <br> Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.). This has no impact on the number of threads used for training. If user does not set it (default), FreqAI will use max number of threads - 2 (leaving 1 physical core available for Freqtrade bot and FreqUI) <br> **Datatype:** Positive integer.
@ -46,13 +45,15 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation). <br> **Datatype:** Integer. <br> Default: `0`. | `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation). <br> **Datatype:** Integer. <br> Default: `0`.
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`. | `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal). | `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).
| `shuffle_after_split` | Split the data into train and test sets, and then shuffle both sets individually. <br> **Datatype:** Boolean. <br> Default: `False`.
| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.<br> **Datatype:** Boolean. <br> Default: `False`.
### Data split parameters ### Data split parameters
| Parameter | Description | | Parameter | Description |
|------------|-------------| |------------|-------------|
| | **Data split parameters within the `freqai.data_split_parameters` sub dictionary** | | **Data split parameters within the `freqai.data_split_parameters` sub dictionary**
| `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary. | `data_split_parameters` | Include any additional parameters available from scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary.
| `test_size` | The fraction of data that should be used for testing instead of training. <br> **Datatype:** Positive float < 1. | `test_size` | The fraction of data that should be used for testing instead of training. <br> **Datatype:** Positive float < 1.
| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`. <br> **Datatype:** Boolean. <br> Defaut: `False`. | `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`. <br> **Datatype:** Boolean. <br> Defaut: `False`.
@ -89,6 +90,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| Parameter | Description | | Parameter | Description |
|------------|-------------| |------------|-------------|
| | **Extraneous parameters** | | **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.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 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.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`. | `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`.

View File

@ -24,7 +24,7 @@ The framework is built on stable_baselines3 (torch) and OpenAI gym for the base
### Important considerations ### Important considerations
As explained above, the agent is "trained" in an artificial trading "environment". In our case, that environment may seem quite similar to a real Freqtrade backtesting environment, but it is *NOT*. In fact, the RL training environment is much more simplified. It does not incorporate any of the complicated strategy logic, such as callbacks like `custom_exit`, `custom_stoploss`, leverage controls, etc. The RL environment is instead a very "raw" representation of the true market, where the agent has free-will to learn the policy (read: stoploss, take profit, etc.) which is enforced by the `calculate_reward()`. Thus, it is important to consider that the agent training environment is not identical to the real world. As explained above, the agent is "trained" in an artificial trading "environment". In our case, that environment may seem quite similar to a real Freqtrade backtesting environment, but it is *NOT*. In fact, the RL training environment is much more simplified. It does not incorporate any of the complicated strategy logic, such as callbacks like `custom_exit`, `custom_stoploss`, leverage controls, etc. The RL environment is instead a very "raw" representation of the true market, where the agent has free will to learn the policy (read: stoploss, take profit, etc.) which is enforced by the `calculate_reward()`. Thus, it is important to consider that the agent training environment is not identical to the real world.
## Running Reinforcement Learning ## Running Reinforcement Learning
@ -175,10 +175,21 @@ As you begin to modify the strategy and the prediction model, you will quickly r
pnl = self.get_unrealized_profit() pnl = self.get_unrealized_profit()
factor = 100 factor = 100
# you can use feature values from dataframe
# Assumes the shifted RSI indicator has been generated in the strategy.
rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{self.pair}_"
f"{self.config['timeframe']}"].iloc[self._current_tick]
# reward agent for entering trades # reward agent for entering trades
if action in (Actions.Long_enter.value, Actions.Short_enter.value) \ if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
and self._position == Positions.Neutral: and self._position == Positions.Neutral):
return 25 if rsi_now < 40:
factor = 40 / rsi_now
else:
factor = 1
return 25 * factor
# discourage agent from not entering trades # discourage agent from not entering trades
if action == Actions.Neutral.value and self._position == Positions.Neutral: if action == Actions.Neutral.value and self._position == Positions.Neutral:
return -1 return -1

View File

@ -120,7 +120,7 @@ In the presented example config, the user will only allow predictions on models
Model training parameters are unique to the selected machine learning library. FreqAI allows you to set any parameter for any library using the `model_training_parameters` dictionary in the config. The example config (found in `config_examples/config_freqai.example.json`) shows some of the example parameters associated with `Catboost` and `LightGBM`, but you can add any parameters available in those libraries or any other machine learning library you choose to implement. Model training parameters are unique to the selected machine learning library. FreqAI allows you to set any parameter for any library using the `model_training_parameters` dictionary in the config. The example config (found in `config_examples/config_freqai.example.json`) shows some of the example parameters associated with `Catboost` and `LightGBM`, but you can add any parameters available in those libraries or any other machine learning library you choose to implement.
Data split parameters are defined in `data_split_parameters` which can be any parameters associated with Scikit-learn's `train_test_split()` function. `train_test_split()` has a parameters called `shuffle` which allows to shuffle the data or keep it unshuffled. This is particularly useful to avoid biasing training with temporally auto-correlated data. More details about these parameters can be found the [Scikit-learn website](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). Data split parameters are defined in `data_split_parameters` which can be any parameters associated with scikit-learn's `train_test_split()` function. `train_test_split()` has a parameters called `shuffle` which allows to shuffle the data or keep it unshuffled. This is particularly useful to avoid biasing training with temporally auto-correlated data. More details about these parameters can be found the [scikit-learn website](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website).
The FreqAI specific parameter `label_period_candles` defines the offset (number of candles into the future) used for the `labels`. In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file), the user is asking for `labels` that are 24 candles in the future. The FreqAI specific parameter `label_period_candles` defines the offset (number of candles into the future) used for the `labels`. In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file), the user is asking for `labels` that are 24 candles in the future.
@ -165,20 +165,3 @@ tensorboard --logdir user_data/models/unique-id
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell if you wish to view the output in your browser at 127.0.0.1:6060 (6060 is the default port used by Tensorboard). where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell if you wish to view the output in your browser at 127.0.0.1:6060 (6060 is the default port used by Tensorboard).
![tensorboard](assets/tensorboard.jpg) ![tensorboard](assets/tensorboard.jpg)
## Setting up a follower
You can indicate to the bot that it should not train models, but instead should look for models trained by a leader with a specific `identifier` by defining:
```json
"freqai": {
"enabled": true,
"follow_mode": true,
"identifier": "example",
"feature_parameters": {
// leader bots feature_parameters inserted here
},
}
```
In this example, the user has a leader bot with the `"identifier": "example"`. The leader bot is already running or is launched simultaneously with the follower. The follower will load models created by the leader and inference them to obtain predictions instead of training its own models. The user will also need to duplicate the `feature_parameters` parameters from from the leaders freqai configuration file into the freqai section of the followers config.

View File

@ -4,7 +4,10 @@
## Introduction ## Introduction
FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, the FreqAI aims to be a sand-box for easily deploying robust machine-learning libraries on real-time data ([details])(#freqai-position-in-open-source-machine-learning-landscape). FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, FreqAI aims to be a sandbox for easily deploying robust machine learning libraries on real-time data ([details](#freqai-position-in-open-source-machine-learning-landscape)).
!!! Note
FreqAI is, and always will be, a not-for-profit, open-source project. FreqAI does *not* have a crypto token, FreqAI does *not* sell signals, and FreqAI does not have a domain besides the present [freqtrade documentation](https://www.freqtrade.io/en/latest/freqai/).
Features include: Features include:
@ -19,7 +22,7 @@ Features include:
* **Automatic data download** - Compute timeranges for data downloads and update historic data (in live deployments) * **Automatic data download** - Compute timeranges for data downloads and update historic data (in live deployments)
* **Cleaning of incoming data** - Handle NaNs safely before training and model inferencing * **Cleaning of incoming data** - Handle NaNs safely before training and model inferencing
* **Dimensionality reduction** - Reduce the size of the training data via [Principal Component Analysis](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis) * **Dimensionality reduction** - Reduce the size of the training data via [Principal Component Analysis](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis)
* **Deploying bot fleets** - Set one bot to train models while a fleet of [follower bots](freqai-running.md#setting-up-a-follower) inference the models and handle trades * **Deploying bot fleets** - Set one bot to train models while a fleet of [consumers](producer-consumer.md) use signals.
## Quick start ## Quick start
@ -70,11 +73,11 @@ pip install -r requirements-freqai.txt
### Usage with docker ### Usage with docker
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker-compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices. If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices.
### FreqAI position in open-source machine learning landscape ### FreqAI position in open-source machine learning landscape
Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`) has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citzen scientists" to use their basic Python skills for data-exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data-collection, storage, and handling presents a disparate challenge. [`FreqAI`](#freqai) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data. Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`) has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citzen scientists" to use their basic Python skills for data exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data collection, storage, and handling presents a disparate challenge. [`FreqAI`](#freqai) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data.
### Citing FreqAI ### Citing FreqAI

View File

@ -52,6 +52,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
- [X] [Binance](https://www.binance.com/) - [X] [Binance](https://www.binance.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [OKX](https://okx.com/) - [X] [OKX](https://okx.com/)
- [X] [Bybit](https://bybit.com/)
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in. Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.

View File

@ -30,6 +30,12 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
!!! Warning "Up-to-date clock" !!! 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. 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 ## Requirements

View File

@ -1,6 +1,6 @@
markdown==3.3.7 markdown==3.3.7
mkdocs==1.4.2 mkdocs==1.4.2
mkdocs-material==9.0.5 mkdocs-material==9.0.13
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==9.9.1 pymdown-extensions==9.9.2
jinja2==3.1.2 jinja2==3.1.2

View File

@ -163,7 +163,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `strategy <strategy>` | Get specific Strategy content. **Alpha** | `strategy <strategy>` | Get specific Strategy content. **Alpha**
| `available_pairs` | List available backtest data. **Alpha** | `available_pairs` | List available backtest data. **Alpha**
| `version` | Show version. | `version` | Show version.
| `sysinfo` | Show informations about the system load. | `sysinfo` | Show information about the system load.
| `health` | Show bot health (last bot loop). | `health` | Show bot health (last bot loop).
!!! Warning "Alpha status" !!! Warning "Alpha status"
@ -192,6 +192,11 @@ blacklist
:param add: List of coins to add (example: "BNB/BTC") :param add: List of coins to add (example: "BNB/BTC")
cancel_open_order
Cancel open order for trade.
:param trade_id: Cancels open orders for this trade.
count count
Return the amount of open trades. Return the amount of open trades.
@ -274,7 +279,6 @@ reload_config
Reload configuration. Reload configuration.
show_config show_config
Returns part of the configuration, relevant for trading operations. Returns part of the configuration, relevant for trading operations.
start start
@ -320,6 +324,7 @@ version
whitelist whitelist
Show the current whitelist. Show the current whitelist.
``` ```
### Message WebSocket ### Message WebSocket

View File

@ -24,7 +24,7 @@ These modes can be configured with these values:
``` ```
!!! Note !!! Note
Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), Gateio (stop-limit), and Kucoin (stop-limit and stop-market) as of now. Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), Gate (stop-limit), and Kucoin (stop-limit and stop-market) as of now.
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins> <ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work. If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
@ -52,6 +52,18 @@ The bot cannot do these every 5 seconds (at each iteration), otherwise it would
So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
### stoploss_price_type
!!! Warning "Only applies to futures"
`stoploss_price_type` only applies to futures markets (on exchanges where it's available).
Freqtrade will perform a validation of this setting on startup, failing to start if an invalid setting for your exchange has been selected.
Supported price types are gonna differs between each exchanges. Please check with your exchange on which price types it supports.
Stoploss on exchange on futures markets can trigger on different price types.
The naming for these prices in exchange terminology often varies, but is usually something around "last" (or "contract price" ), "mark" and "index".
Acceptable values for this setting are `"last"`, `"mark"` and `"index"` - which freqtrade will transfer automatically to the corresponding API type, and place the [stoploss on exchange](#stoploss_on_exchange-and-stoploss_on_exchange_limit_ratio) order correspondingly.
### force_exit ### force_exit
`force_exit` is an optional value, which defaults to the same value as `exit` and is used when sending a `/forceexit` command from Telegram or from the Rest API. `force_exit` is an optional value, which defaults to the same value as `exit` and is used when sending a `/forceexit` command from Telegram or from the Rest API.

View File

@ -80,6 +80,7 @@ from freqtrade.resolvers import StrategyResolver
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
strategy = StrategyResolver.load_strategy(config) strategy = StrategyResolver.load_strategy(config)
strategy.dp = DataProvider(config, None, None) strategy.dp = DataProvider(config, None, None)
strategy.ft_bot_start()
# Generate buy/sell signals using strategy # Generate buy/sell signals using strategy
df = strategy.analyze_ticker(candles, {'pair': pair}) df = strategy.analyze_ticker(candles, {'pair': pair})

View File

@ -162,26 +162,33 @@ official commands. You can ask at any moment for help with `/help`.
| Command | Description | | Command | Description |
|----------|-------------| |----------|-------------|
| **System commands**
| `/start` | Starts the trader | `/start` | Starts the trader
| `/stop` | Stops the trader | `/stop` | Stops the trader
| `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `/reload_config` | Reloads the configuration file | `/reload_config` | Reloads the configuration file
| `/show_config` | Shows part of the current configuration with relevant settings to operation | `/show_config` | Shows part of the current configuration with relevant settings to operation
| `/logs [limit]` | Show last log messages. | `/logs [limit]` | Show last log messages.
| `/help` | Show help message
| `/version` | Show version
| **Status** |
| `/status` | Lists all open trades | `/status` | Lists all open trades
| `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space. | `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space.
| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) | `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
| `/trades [limit]` | List all recently closed trades in a table format. | `/trades [limit]` | List all recently closed trades in a table format.
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
| `/count` | Displays number of trades used and available | `/count` | Displays number of trades used and available
| `/locks` | Show currently locked pairs. | `/locks` | Show currently locked pairs.
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id). | `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
| `/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) | **Modify Trade states** |
| `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`). | `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`). | `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `/fx` | alias for `/forceexit` | `/fx` | alias for `/forceexit`
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True) | `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True) | `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
| `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade.
| **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 | `/performance` | Show performance of each finished trade grouped by pair
| `/balance` | Show account balance per currency | `/balance` | Show account balance per currency
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7) | `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
@ -193,8 +200,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing. | `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing.
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
| `/edge` | Show validated pairs by Edge if it is enabled. | `/edge` | Show validated pairs by Edge if it is enabled.
| `/help` | Show help message
| `/version` | Show version
## Telegram commands in action ## Telegram commands in action

View File

@ -12,7 +12,7 @@ dependencies:
- py-find-1st - py-find-1st
- aiohttp - aiohttp
- SQLAlchemy - SQLAlchemy
- python-telegram-bot - python-telegram-bot<20.0.0
- arrow - arrow
- cachetools - cachetools
- requests - requests
@ -54,7 +54,7 @@ dependencies:
# 3/4 req hyperopt # 3/4 req hyperopt
- scipy - scipy
- scikit-learn - scikit-learn<1.2.0
- filelock - filelock
- scikit-optimize - scikit-optimize
- progressbar2 - progressbar2

View File

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

0
freqtrade/__main__.py Normal file → Executable file
View File

0
freqtrade/commands/analyze_commands.py Executable file → Normal file
View File

View File

@ -108,7 +108,7 @@ def ask_user_config() -> Dict[str, Any]:
"binance", "binance",
"binanceus", "binanceus",
"bittrex", "bittrex",
"gateio", "gate",
"huobi", "huobi",
"kraken", "kraken",
"kucoin", "kucoin",
@ -123,7 +123,7 @@ def ask_user_config() -> Dict[str, Any]:
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?", "message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
"default": False, "default": False,
"filter": lambda val: 'futures' if val else 'spot', "filter": lambda val: 'futures' if val else 'spot',
"when": lambda x: x["exchange_name"] in ['binance', 'gateio', 'okx'], "when": lambda x: x["exchange_name"] in ['binance', 'gate', 'okx'],
}, },
{ {
"type": "autocomplete", "type": "autocomplete",

0
freqtrade/commands/hyperopt_commands.py Executable file → Normal file
View File

View File

@ -1,4 +1,5 @@
import logging import logging
import signal
from typing import Any, Dict from typing import Any, Dict
@ -12,15 +13,20 @@ def start_trading(args: Dict[str, Any]) -> int:
# Import here to avoid loading worker module when it's not used # Import here to avoid loading worker module when it's not used
from freqtrade.worker import Worker from freqtrade.worker import Worker
def term_handler(signum, frame):
# Raise KeyboardInterrupt - so we can handle it in the same way as Ctrl-C
raise KeyboardInterrupt()
# Create and run worker # Create and run worker
worker = None worker = None
try: try:
signal.signal(signal.SIGTERM, term_handler)
worker = Worker(args) worker = Worker(args)
worker.run() worker.run()
except Exception as e: except Exception as e:
logger.error(str(e)) logger.error(str(e))
logger.exception("Fatal exception!") logger.exception("Fatal exception!")
except KeyboardInterrupt: except (KeyboardInterrupt):
logger.info('SIGINT received, aborting ...') logger.info('SIGINT received, aborting ...')
finally: finally:
if worker: if worker:

View File

@ -32,7 +32,7 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
:param prefix: Prefix to consider (usually FREQTRADE__) :param prefix: Prefix to consider (usually FREQTRADE__)
:return: Nested dict based on available and relevant variables. :return: Nested dict based on available and relevant variables.
""" """
no_convert = ['CHAT_ID'] no_convert = ['CHAT_ID', 'PASSWORD']
relevant_vars: Dict[str, Any] = {} relevant_vars: Dict[str, Any] = {}
for env_var, val in sorted(env_dict.items()): for env_var, val in sorted(env_dict.items()):

View File

@ -5,7 +5,7 @@ bot constants
""" """
from typing import Any, Dict, List, Literal, Tuple from typing import Any, Dict, List, Literal, Tuple
from freqtrade.enums import CandleType, RPCMessageType from freqtrade.enums import CandleType, PriceType, RPCMessageType
DEFAULT_CONFIG = 'config.json' DEFAULT_CONFIG = 'config.json'
@ -25,6 +25,7 @@ PRICING_SIDES = ['ask', 'bid', 'same', 'other']
ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTYPE_POSSIBILITIES = ['limit', 'market']
_ORDERTIF_POSSIBILITIES = ['GTC', 'FOK', 'IOC', 'PO'] _ORDERTIF_POSSIBILITIES = ['GTC', 'FOK', 'IOC', 'PO']
ORDERTIF_POSSIBILITIES = _ORDERTIF_POSSIBILITIES + [t.lower() for t in _ORDERTIF_POSSIBILITIES] ORDERTIF_POSSIBILITIES = _ORDERTIF_POSSIBILITIES + [t.lower() for t in _ORDERTIF_POSSIBILITIES]
STOPLOSS_PRICE_TYPES = [p for p in PriceType]
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
@ -229,6 +230,7 @@ CONF_SCHEMA = {
'default': 'market'}, 'default': 'market'},
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss_on_exchange': {'type': 'boolean'}, 'stoploss_on_exchange': {'type': 'boolean'},
'stoploss_price_type': {'type': 'string', 'enum': STOPLOSS_PRICE_TYPES},
'stoploss_on_exchange_interval': {'type': 'number'}, 'stoploss_on_exchange_interval': {'type': 'number'},
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0, 'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
'maximum': 1.0} 'maximum': 1.0}
@ -544,7 +546,7 @@ CONF_SCHEMA = {
"enabled": {"type": "boolean", "default": False}, "enabled": {"type": "boolean", "default": False},
"keras": {"type": "boolean", "default": False}, "keras": {"type": "boolean", "default": False},
"write_metrics_to_disk": {"type": "boolean", "default": False}, "write_metrics_to_disk": {"type": "boolean", "default": False},
"purge_old_models": {"type": "boolean", "default": True}, "purge_old_models": {"type": ["boolean", "number"], "default": 2},
"conv_width": {"type": "integer", "default": 1}, "conv_width": {"type": "integer", "default": 1},
"train_period_days": {"type": "integer", "default": 0}, "train_period_days": {"type": "integer", "default": 0},
"backtest_period_days": {"type": "number", "default": 7}, "backtest_period_days": {"type": "number", "default": 7},
@ -566,7 +568,9 @@ CONF_SCHEMA = {
"shuffle": {"type": "boolean", "default": False}, "shuffle": {"type": "boolean", "default": False},
"nu": {"type": "number", "default": 0.1} "nu": {"type": "number", "default": 0.1}
}, },
} },
"shuffle_after_split": {"type": "boolean", "default": False},
"buffer_train_data_candles": {"type": "integer", "default": 0}
}, },
"required": ["include_timeframes", "include_corr_pairlist", ] "required": ["include_timeframes", "include_corr_pairlist", ]
}, },
@ -679,6 +683,7 @@ EntryExit = Literal['entry', 'exit']
BuySell = Literal['buy', 'sell'] BuySell = Literal['buy', 'sell']
MakerTaker = Literal['maker', 'taker'] MakerTaker = Literal['maker', 'taker']
BidAsk = Literal['bid', 'ask'] BidAsk = Literal['bid', 'ask']
OBLiteral = Literal['asks', 'bids']
Config = Dict[str, Any] Config = Dict[str, Any]
IntOrInf = float IntOrInf = float

View File

@ -9,7 +9,7 @@ from collections import deque
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from pandas import DataFrame, to_timedelta from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes, from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes,
@ -18,6 +18,7 @@ from freqtrade.data.history import load_pair_history
from freqtrade.enums import CandleType, RPCMessageType, RunMode from freqtrade.enums import CandleType, RPCMessageType, RunMode
from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.exchange import Exchange, timeframe_to_seconds from freqtrade.exchange import Exchange, timeframe_to_seconds
from freqtrade.exchange.types import OrderBook
from freqtrade.misc import append_candles_to_dataframe from freqtrade.misc import append_candles_to_dataframe
from freqtrade.rpc import RPCManager from freqtrade.rpc import RPCManager
from freqtrade.util import PeriodicCache from freqtrade.util import PeriodicCache
@ -206,9 +207,11 @@ class DataProvider:
existing_df, _ = self.__producer_pairs_df[producer_name][pair_key] existing_df, _ = self.__producer_pairs_df[producer_name][pair_key]
# CHECK FOR MISSING CANDLES # CHECK FOR MISSING CANDLES
timeframe_delta = to_timedelta(timeframe) # Convert the timeframe to a timedelta for pandas # Convert the timeframe to a timedelta for pandas
local_last = existing_df.iloc[-1]['date'] # We want the last date from our copy timeframe_delta: Timedelta = to_timedelta(timeframe)
incoming_first = dataframe.iloc[0]['date'] # We want the first date from the incoming local_last: Timestamp = existing_df.iloc[-1]['date'] # We want the last date from our copy
# We want the first date from the incoming
incoming_first: Timestamp = dataframe.iloc[0]['date']
# Remove existing candles that are newer than the incoming first candle # Remove existing candles that are newer than the incoming first candle
existing_df1 = existing_df[existing_df['date'] < incoming_first] existing_df1 = existing_df[existing_df['date'] < incoming_first]
@ -221,7 +224,7 @@ class DataProvider:
# we missed some candles between our data and the incoming # we missed some candles between our data and the incoming
# so return False and candle_difference. # so return False and candle_difference.
if candle_difference > 1: if candle_difference > 1:
return (False, candle_difference) return (False, int(candle_difference))
if existing_df1.empty: if existing_df1.empty:
appended_df = dataframe appended_df = dataframe
else: else:
@ -421,10 +424,8 @@ class DataProvider:
""" """
if self._exchange is None: if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION) raise OperationalException(NO_EXCHANGE_EXCEPTION)
if helping_pairs: final_pairs = (pairlist + helping_pairs) if helping_pairs else pairlist
self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs) self._exchange.refresh_latest_ohlcv(final_pairs)
else:
self._exchange.refresh_latest_ohlcv(pairlist)
@property @property
def available_pairs(self) -> ListPairsWithTimeframes: def available_pairs(self) -> ListPairsWithTimeframes:
@ -487,7 +488,7 @@ class DataProvider:
except ExchangeError: except ExchangeError:
return {} return {}
def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: def orderbook(self, pair: str, maximum: int) -> OrderBook:
""" """
Fetch latest l2 orderbook data Fetch latest l2 orderbook data
Warning: Does a network request - so use with common sense. Warning: Does a network request - so use with common sense.

0
freqtrade/data/entryexitanalysis.py Executable file → Normal file
View File

View File

@ -308,7 +308,7 @@ class IDataHandler(ABC):
timerange=timerange_startup, timerange=timerange_startup,
candle_type=candle_type candle_type=candle_type
) )
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True): if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
return pairdf return pairdf
else: else:
enddate = pairdf.iloc[-1]['date'] enddate = pairdf.iloc[-1]['date']
@ -316,7 +316,7 @@ class IDataHandler(ABC):
if timerange_startup: if timerange_startup:
self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup) self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup)
pairdf = trim_dataframe(pairdf, timerange_startup) pairdf = trim_dataframe(pairdf, timerange_startup)
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data): if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True):
return pairdf return pairdf
# incomplete candles should only be dropped if we didn't trim the end beforehand. # incomplete candles should only be dropped if we didn't trim the end beforehand.

View File

@ -195,7 +195,7 @@ class Edge:
def stake_amount(self, pair: str, free_capital: float, def stake_amount(self, pair: str, free_capital: float,
total_capital: float, capital_in_trade: float) -> float: total_capital: float, capital_in_trade: float) -> float:
stoploss = self.stoploss(pair) stoploss = self.get_stoploss(pair)
available_capital = (total_capital + capital_in_trade) * self._capital_ratio available_capital = (total_capital + capital_in_trade) * self._capital_ratio
allowed_capital_at_risk = available_capital * self._allowed_risk allowed_capital_at_risk = available_capital * self._allowed_risk
max_position_size = abs(allowed_capital_at_risk / stoploss) max_position_size = abs(allowed_capital_at_risk / stoploss)
@ -214,7 +214,7 @@ class Edge:
) )
return round(position_size, 15) return round(position_size, 15)
def stoploss(self, pair: str) -> float: def get_stoploss(self, pair: str) -> float:
if pair in self._cached_pairs: if pair in self._cached_pairs:
return self._cached_pairs[pair].stoploss return self._cached_pairs[pair].stoploss
else: else:

View File

@ -6,6 +6,7 @@ from freqtrade.enums.exittype import ExitType
from freqtrade.enums.hyperoptstate import HyperoptState from freqtrade.enums.hyperoptstate import HyperoptState
from freqtrade.enums.marginmode import MarginMode from freqtrade.enums.marginmode import MarginMode
from freqtrade.enums.ordertypevalue import OrderTypeValues from freqtrade.enums.ordertypevalue import OrderTypeValues
from freqtrade.enums.pricetype import PriceType
from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType

View File

@ -0,0 +1,8 @@
from enum import Enum
class PriceType(str, Enum):
"""Enum to distinguish possible trigger prices for stoplosses"""
LAST = "last"
MARK = "mark"
INDEX = "index"

View File

@ -17,7 +17,7 @@ from freqtrade.exchange.exchange_utils import (amount_to_contract_precision, amo
timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_next_date, timeframe_to_prev_date,
timeframe_to_seconds, validate_exchange, timeframe_to_seconds, validate_exchange,
validate_exchanges) validate_exchanges)
from freqtrade.exchange.gateio import Gateio from freqtrade.exchange.gate import Gate
from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.hitbtc import Hitbtc
from freqtrade.exchange.huobi import Huobi from freqtrade.exchange.huobi import Huobi
from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kraken import Kraken

View File

@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Tuple
import arrow import arrow
import ccxt import ccxt
from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
@ -28,11 +28,16 @@ class Binance(Exchange):
"trades_pagination": "id", "trades_pagination": "id",
"trades_pagination_arg": "fromId", "trades_pagination_arg": "fromId",
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
"ccxt_futures_name": "swap"
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"stoploss_order_types": {"limit": "stop", "market": "stop_market"}, "stoploss_order_types": {"limit": "stop", "market": "stop_market"},
"tickers_have_price": False, "tickers_have_price": False,
"floor_leverage": True,
"stop_price_type_field": "workingType",
"stop_price_type_value_mapping": {
PriceType.LAST: "CONTRACT_PRICE",
PriceType.MARK: "MARK_PRICE",
},
} }
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
@ -78,33 +83,9 @@ class Binance(Exchange):
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}'
except ccxt.BaseError as e: ) from e
raise OperationalException(e) from e
@retrier
def _set_leverage(
self,
leverage: float,
pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None
):
"""
Set's the leverage before making a trade, in order to not
have the same leverage on every trade
"""
trading_mode = trading_mode or self.trading_mode
if self._config['dry_run'] or trading_mode != TradingMode.FUTURES:
return
try:
self._api.set_leverage(symbol=pair, leverage=round(leverage))
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
@ -150,6 +131,7 @@ class Binance(Exchange):
is_short: bool, is_short: bool,
amount: float, amount: float,
stake_amount: float, stake_amount: float,
leverage: float,
wallet_balance: float, # Or margin balance wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only upnl_ex_1: float = 0.0, # (Binance) Cross only
@ -159,11 +141,12 @@ class Binance(Exchange):
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
:param exchange_name: :param pair: Pair to calculate liquidation price for
:param open_rate: Entry price of position :param open_rate: Entry price of position
:param is_short: True if the trade is a short, false otherwise :param is_short: True if the trade is a short, false otherwise
:param amount: Absolute value of position size incl. leverage (in base currency) :param amount: Absolute value of position size incl. leverage (in base currency)
:param stake_amount: Stake amount - Collateral in settle currency. :param stake_amount: Stake amount - Collateral in settle currency.
:param leverage: Leverage used for this position.
:param trading_mode: SPOT, MARGIN, FUTURES, etc. :param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param margin_mode: Either ISOLATED or CROSS :param margin_mode: Either ISOLATED or CROSS
:param wallet_balance: Amount of margin_mode in the wallet being used to trade :param wallet_balance: Amount of margin_mode in the wallet being used to trade

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,16 @@
""" Bybit exchange subclass """ """ Bybit exchange subclass """
import logging import logging
from typing import Dict, List, Tuple from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from freqtrade.enums import MarginMode, TradingMode import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, PriceType, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_utils import timeframe_to_msecs
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,17 +28,27 @@ class Bybit(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"ohlcv_candle_limit": 1000, "ohlcv_candle_limit": 1000,
"ccxt_futures_name": "linear",
"ohlcv_has_history": False, "ohlcv_has_history": False,
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"ohlcv_candle_limit": 200,
"ohlcv_has_history": True, "ohlcv_has_history": True,
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit", "market": "market"},
"stop_price_type_field": "triggerBy",
"stop_price_type_value_mapping": {
PriceType.LAST: "LastPrice",
PriceType.MARK: "MarkPrice",
PriceType.INDEX: "IndexPrice",
},
} }
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list # TradingMode.SPOT always supported and not required in this list
# (TradingMode.FUTURES, MarginMode.CROSS), # (TradingMode.FUTURES, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.ISOLATED) (TradingMode.FUTURES, MarginMode.ISOLATED)
] ]
@property @property
@ -47,3 +64,158 @@ class Bybit(Exchange):
}) })
config.update(super()._ccxt_config) config.update(super()._ccxt_config)
return config return config
def market_is_future(self, market: Dict[str, Any]) -> bool:
main = super().market_is_future(market)
# For ByBit, we'll only support USDT markets for now.
return (
main and market['settle'] == 'USDT'
)
@retrier
def additional_exchange_init(self) -> None:
"""
Additional exchange initialization logic.
.api will be available at this point.
Must be overridden in child methods if required.
"""
try:
if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']:
position_mode = self._api.set_position_mode(False)
self._log_exchange_response('set_position_mode', position_mode)
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}'
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
async def _fetch_funding_rate_history(
self,
pair: str,
timeframe: str,
limit: int,
since_ms: Optional[int] = None,
) -> List[List]:
"""
Fetch funding rate history
Necessary workaround until https://github.com/ccxt/ccxt/issues/15990 is fixed.
"""
params = {}
if since_ms:
until = since_ms + (timeframe_to_msecs(timeframe) * self._ft_has['ohlcv_candle_limit'])
params.update({'until': until})
# Funding rate
data = await self._api_async.fetch_funding_rate_history(
pair, since=since_ms,
params=params)
# Convert funding rate to candle pattern
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
return data
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
if self.trading_mode != TradingMode.SPOT:
params = {'leverage': leverage}
self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)
self._set_leverage(leverage, pair, accept_fail=True)
def _get_params(
self,
side: BuySell,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'GTC',
) -> Dict:
params = super()._get_params(
side=side,
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['position_idx'] = 0
return params
def dry_run_liquidation_price(
self,
pair: str,
open_rate: float, # Entry price of position
is_short: bool,
amount: float,
stake_amount: float,
leverage: float,
wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
Important: Must be fetching data from cached values as this is used by backtesting!
PERPETUAL:
bybit:
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
Long:
Liquidation Price = (
Entry Price * (1 - Initial Margin Rate + Maintenance Margin Rate)
- Extra Margin Added/ Contract)
Short:
Liquidation Price = (
Entry Price * (1 + Initial Margin Rate - Maintenance Margin Rate)
+ Extra Margin Added/ Contract)
Implementation Note: Extra margin is currently not used.
:param pair: Pair to calculate liquidation price for
:param open_rate: Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param amount: Absolute value of position size incl. leverage (in base currency)
:param stake_amount: Stake amount - Collateral in settle currency.
:param leverage: Leverage used for this position.
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param margin_mode: Either ISOLATED or CROSS
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
"""
market = self.markets[pair]
mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
if market['inverse']:
raise OperationalException(
"Freqtrade does not yet support inverse contracts")
initial_margin_rate = 1 / leverage
# See docstring - ignores extra margin!
if is_short:
return open_rate * (1 + initial_margin_rate - mm_ratio)
else:
return open_rate * (1 - initial_margin_rate + mm_ratio)
else:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading")
def get_funding_fees(
self, pair: str, amount: float, is_short: bool, open_date: datetime) -> float:
"""
Fetch funding fees, either from the exchange (live) or calculates them
based on funding rate/mark price history
:param pair: The quote/base pair of the trade
:param is_short: trade direction
:param amount: Trade amount
:param open_date: Open date of the trade
:return: funding fee since open_date
:raises: ExchangeError if something goes wrong.
"""
# Bybit does not provide "applied" funding fees per position.
if self.trading_mode == TradingMode.FUTURES:
return self._fetch_and_calculate_funding_fees(
pair, amount, is_short, open_date)
return 0.0

View File

@ -46,13 +46,13 @@ MAP_EXCHANGE_CHILDCLASS = {
'binanceje': 'binance', 'binanceje': 'binance',
'binanceusdm': 'binance', 'binanceusdm': 'binance',
'okex': 'okx', 'okex': 'okx',
'gate': 'gateio', 'gateio': 'gate',
} }
SUPPORTED_EXCHANGES = [ SUPPORTED_EXCHANGES = [
'binance', 'binance',
'bittrex', 'bittrex',
'gateio', 'gate',
'huobi', 'huobi',
'kraken', 'kraken',
'okx', 'okx',

View File

@ -7,6 +7,7 @@ import inspect
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import floor
from threading import Lock from threading import Lock
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
@ -20,9 +21,10 @@ from pandas import DataFrame, concat
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk, from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk,
BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker, BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker,
PairWithTimeframe) OBLiteral, PairWithTimeframe)
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
from freqtrade.enums.pricetype import PriceType
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError, InvalidOrderException, OperationalException, PricingError,
RetryableOrderError, TemporaryError) RetryableOrderError, TemporaryError)
@ -35,7 +37,7 @@ from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contrac
price_to_precision, timeframe_to_minutes, price_to_precision, timeframe_to_minutes,
timeframe_to_msecs, timeframe_to_next_date, timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_prev_date, timeframe_to_seconds) timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.exchange.types import OHLCVResponse, Ticker, Tickers from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json, from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
safe_value_fallback2) safe_value_fallback2)
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
@ -599,12 +601,27 @@ class Exchange:
if not self.exchange_has('createMarketOrder'): if not self.exchange_has('createMarketOrder'):
raise OperationalException( raise OperationalException(
f'Exchange {self.name} does not support market orders.') f'Exchange {self.name} does not support market orders.')
self.validate_stop_ordertypes(order_types)
def validate_stop_ordertypes(self, order_types: Dict) -> None:
"""
Validate stoploss order types
"""
if (order_types.get("stoploss_on_exchange") if (order_types.get("stoploss_on_exchange")
and not self._ft_has.get("stoploss_on_exchange", False)): and not self._ft_has.get("stoploss_on_exchange", False)):
raise OperationalException( raise OperationalException(
f'On exchange stoploss is not supported for {self.name}.' f'On exchange stoploss is not supported for {self.name}.'
) )
if self.trading_mode == TradingMode.FUTURES:
price_mapping = self._ft_has.get('stop_price_type_value_mapping', {}).keys()
if (
order_types.get("stoploss_on_exchange", False) is True
and 'stoploss_price_type' in order_types
and order_types['stoploss_price_type'] not in price_mapping
):
raise OperationalException(
f'On exchange stoploss price type is not supported for {self.name}.'
)
def validate_pricing(self, pricing: Dict) -> None: def validate_pricing(self, pricing: Dict) -> None:
if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'): if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'):
@ -833,7 +850,7 @@ class Exchange:
'remaining': _amount, 'remaining': _amount,
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'), 'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
'timestamp': arrow.utcnow().int_timestamp * 1000, 'timestamp': arrow.utcnow().int_timestamp * 1000,
'status': "closed" if ordertype == "market" and not stop_loss else "open", 'status': "open",
'fee': None, 'fee': None,
'info': {}, 'info': {},
'leverage': leverage 'leverage': leverage
@ -843,20 +860,33 @@ class Exchange:
dry_order["stopPrice"] = dry_order["price"] dry_order["stopPrice"] = dry_order["price"]
# Workaround to avoid filling stoploss orders immediately # Workaround to avoid filling stoploss orders immediately
dry_order["ft_order_type"] = "stoploss" dry_order["ft_order_type"] = "stoploss"
orderbook: Optional[OrderBook] = None
if self.exchange_has('fetchL2OrderBook'):
orderbook = self.fetch_l2_order_book(pair, 20)
if ordertype == "limit" and orderbook:
# Allow a 3% price difference
allowed_diff = 0.03
if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
logger.info(
f"Converted order {pair} to market order due to price {rate} crossing spread "
f"by more than {allowed_diff:.2%}.")
dry_order["type"] = "market"
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"): if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
# Update market order pricing # Update market order pricing
average = self.get_dry_market_fill_price(pair, side, amount, rate) average = self.get_dry_market_fill_price(pair, side, amount, rate, orderbook)
dry_order.update({ dry_order.update({
'average': average, 'average': average,
'filled': _amount, 'filled': _amount,
'remaining': 0.0, 'remaining': 0.0,
'status': "closed",
'cost': (dry_order['amount'] * average) / leverage 'cost': (dry_order['amount'] * average) / leverage
}) })
# market orders will always incurr taker fees # market orders will always incurr taker fees
dry_order = self.add_dry_order_fee(pair, dry_order, 'taker') dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
dry_order = self.check_dry_limit_order_filled(dry_order, immediate=True) dry_order = self.check_dry_limit_order_filled(
dry_order, immediate=True, orderbook=orderbook)
self._dry_run_open_orders[dry_order["id"]] = dry_order self._dry_run_open_orders[dry_order["id"]] = dry_order
# Copy order and close it - so the returned order is open unless it's a market order # Copy order and close it - so the returned order is open unless it's a market order
@ -878,20 +908,22 @@ class Exchange:
}) })
return dry_order return dry_order
def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float: def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float,
orderbook: Optional[OrderBook]) -> float:
""" """
Get the market order fill price based on orderbook interpolation Get the market order fill price based on orderbook interpolation
""" """
if self.exchange_has('fetchL2OrderBook'): if self.exchange_has('fetchL2OrderBook'):
ob = self.fetch_l2_order_book(pair, 20) if not orderbook:
ob_type = 'asks' if side == 'buy' else 'bids' orderbook = self.fetch_l2_order_book(pair, 20)
ob_type: OBLiteral = 'asks' if side == 'buy' else 'bids'
slippage = 0.05 slippage = 0.05
max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage)) max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
remaining_amount = amount remaining_amount = amount
filled_amount = 0.0 filled_amount = 0.0
book_entry_price = 0.0 book_entry_price = 0.0
for book_entry in ob[ob_type]: for book_entry in orderbook[ob_type]:
book_entry_price = book_entry[0] book_entry_price = book_entry[0]
book_entry_coin_volume = book_entry[1] book_entry_coin_volume = book_entry[1]
if remaining_amount > 0: if remaining_amount > 0:
@ -919,20 +951,20 @@ class Exchange:
return rate return rate
def _is_dry_limit_order_filled(self, pair: str, side: str, limit: float) -> bool: def _dry_is_price_crossed(self, pair: str, side: str, limit: float,
orderbook: Optional[OrderBook] = None, offset: float = 0.0) -> bool:
if not self.exchange_has('fetchL2OrderBook'): if not self.exchange_has('fetchL2OrderBook'):
return True return True
ob = self.fetch_l2_order_book(pair, 1) if not orderbook:
orderbook = self.fetch_l2_order_book(pair, 1)
try: try:
if side == 'buy': if side == 'buy':
price = ob['asks'][0][0] price = orderbook['asks'][0][0]
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}") if limit * (1 - offset) >= price:
if limit >= price:
return True return True
else: else:
price = ob['bids'][0][0] price = orderbook['bids'][0][0]
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}") if limit * (1 + offset) <= price:
if limit <= price:
return True return True
except IndexError: except IndexError:
# Ignore empty orderbooks when filling - can be filled with the next iteration. # Ignore empty orderbooks when filling - can be filled with the next iteration.
@ -940,7 +972,8 @@ class Exchange:
return False return False
def check_dry_limit_order_filled( def check_dry_limit_order_filled(
self, order: Dict[str, Any], immediate: bool = False) -> Dict[str, Any]: self, order: Dict[str, Any], immediate: bool = False,
orderbook: Optional[OrderBook] = None) -> Dict[str, Any]:
""" """
Check dry-run limit order fill and update fee (if it filled). Check dry-run limit order fill and update fee (if it filled).
""" """
@ -948,7 +981,7 @@ class Exchange:
and order['type'] in ["limit"] and order['type'] in ["limit"]
and not order.get('ft_order_type')): and not order.get('ft_order_type')):
pair = order['symbol'] pair = order['symbol']
if self._is_dry_limit_order_filled(pair, order['side'], order['price']): if self._dry_is_price_crossed(pair, order['side'], order['price'], orderbook):
order.update({ order.update({
'status': 'closed', 'status': 'closed',
'filled': order['amount'], 'filled': order['amount'],
@ -1114,8 +1147,8 @@ class Exchange:
return params return params
@retrier(retries=0) @retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, def create_stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict,
side: BuySell, leverage: float) -> Dict: side: BuySell, leverage: float) -> Dict:
""" """
creates a stoploss order. creates a stoploss order.
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
@ -1160,6 +1193,10 @@ class Exchange:
stop_price=stop_price_norm) stop_price=stop_price_norm)
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
params['reduceOnly'] = True params['reduceOnly'] = True
if 'stoploss_price_type' in order_types and 'stop_price_type_field' in self._ft_has:
price_type = self._ft_has['stop_price_type_value_mapping'][
order_types.get('stoploss_price_type', PriceType.LAST)]
params[self._ft_has['stop_price_type_field']] = price_type
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)) amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
@ -1490,7 +1527,7 @@ class Exchange:
return result return result
@retrier @retrier
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: def fetch_l2_order_book(self, pair: str, limit: int = 100) -> OrderBook:
""" """
Get L2 order book from exchange. Get L2 order book from exchange.
Can be limited to a certain amount (if supported). Can be limited to a certain amount (if supported).
@ -1533,7 +1570,7 @@ class Exchange:
def get_rate(self, pair: str, refresh: bool, def get_rate(self, pair: str, refresh: bool,
side: EntryExit, is_short: bool, side: EntryExit, is_short: bool,
order_book: Optional[dict] = None, ticker: Optional[Ticker] = None) -> float: order_book: Optional[OrderBook] = None, ticker: Optional[Ticker] = None) -> float:
""" """
Calculates bid/ask target Calculates bid/ask target
bid rate - between current ask price and last price bid rate - between current ask price and last price
@ -1571,7 +1608,8 @@ class Exchange:
logger.debug('order_book %s', order_book) logger.debug('order_book %s', order_book)
# top 1 = index 0 # top 1 = index 0
try: try:
rate = order_book[f"{price_side}s"][order_book_top - 1][0] obside: OBLiteral = 'bids' if price_side == 'bid' else 'asks'
rate = order_book[obside][order_book_top - 1][0]
except (IndexError, KeyError) as e: except (IndexError, KeyError) as e:
logger.warning( logger.warning(
f"{pair} - {name} Price at location {order_book_top} from orderbook " f"{pair} - {name} Price at location {order_book_top} from orderbook "
@ -1977,9 +2015,9 @@ class Exchange:
continue continue
# Deconstruct tuple (has 5 elements) # Deconstruct tuple (has 5 elements)
pair, timeframe, c_type, ticks, drop_hint = res pair, timeframe, c_type, ticks, drop_hint = res
drop_incomplete = drop_hint if drop_incomplete is None else drop_incomplete drop_incomplete_ = drop_hint if drop_incomplete is None else drop_incomplete
ohlcv_df = self._process_ohlcv_df( ohlcv_df = self._process_ohlcv_df(
pair, timeframe, c_type, ticks, cache, drop_incomplete) pair, timeframe, c_type, ticks, cache, drop_incomplete_)
results_df[(pair, timeframe, c_type)] = ohlcv_df results_df[(pair, timeframe, c_type)] = ohlcv_df
@ -2484,7 +2522,8 @@ class Exchange:
self, self,
leverage: float, leverage: float,
pair: Optional[str] = None, pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False,
): ):
""" """
Set's the leverage before making a trade, in order to not Set's the leverage before making a trade, in order to not
@ -2493,12 +2532,18 @@ class Exchange:
if self._config['dry_run'] or not self.exchange_has("setLeverage"): if self._config['dry_run'] or not self.exchange_has("setLeverage"):
# Some exchanges only support one margin_mode type # Some exchanges only support one margin_mode type
return return
if self._ft_has.get('floor_leverage', False) is True:
# Rounding for binance ...
leverage = floor(leverage)
try: try:
res = self._api.set_leverage(symbol=pair, leverage=leverage) res = self._api.set_leverage(symbol=pair, leverage=leverage)
self._log_exchange_response('set_leverage', res) self._log_exchange_response('set_leverage', res)
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except ccxt.BadRequest as e:
if not accept_fail:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
@ -2520,7 +2565,8 @@ class Exchange:
return open_date.minute > 0 or open_date.second > 0 return open_date.minute > 0 or open_date.second > 0
@retrier @retrier
def set_margin_mode(self, pair: str, margin_mode: MarginMode, params: dict = {}): def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False,
params: dict = {}):
""" """
Set's the margin mode on the exchange to cross or isolated for a specific pair Set's the margin mode on the exchange to cross or isolated for a specific pair
:param pair: base/quote currency pair (e.g. "ADA/USDT") :param pair: base/quote currency pair (e.g. "ADA/USDT")
@ -2534,6 +2580,10 @@ class Exchange:
self._log_exchange_response('set_margin_mode', res) self._log_exchange_response('set_margin_mode', res)
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except ccxt.BadRequest as e:
if not accept_fail:
raise TemporaryError(
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
@ -2687,6 +2737,7 @@ class Exchange:
is_short: bool, is_short: bool,
amount: float, # Absolute value of position size amount: float, # Absolute value of position size
stake_amount: float, stake_amount: float,
leverage: float,
wallet_balance: float, wallet_balance: float,
mm_ex_1: float = 0.0, # (Binance) Cross only mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only upnl_ex_1: float = 0.0, # (Binance) Cross only
@ -2708,6 +2759,7 @@ class Exchange:
open_rate=open_rate, open_rate=open_rate,
is_short=is_short, is_short=is_short,
amount=amount, amount=amount,
leverage=leverage,
stake_amount=stake_amount, stake_amount=stake_amount,
wallet_balance=wallet_balance, wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1, mm_ex_1=mm_ex_1,
@ -2719,7 +2771,7 @@ class Exchange:
pos = positions[0] pos = positions[0]
isolated_liq = pos['liquidationPrice'] isolated_liq = pos['liquidationPrice']
if isolated_liq: if isolated_liq is not None:
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
isolated_liq = ( isolated_liq = (
isolated_liq - buffer_amount isolated_liq - buffer_amount
@ -2737,6 +2789,7 @@ class Exchange:
is_short: bool, is_short: bool,
amount: float, amount: float,
stake_amount: float, stake_amount: float,
leverage: float,
wallet_balance: float, # Or margin balance wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only upnl_ex_1: float = 0.0, # (Binance) Cross only
@ -2744,7 +2797,7 @@ class Exchange:
""" """
Important: Must be fetching data from cached values as this is used by backtesting! Important: Must be fetching data from cached values as this is used by backtesting!
PERPETUAL: PERPETUAL:
gateio: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price gate: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price
> Liquidation Price = (Entry Price ± Margin / Contract Multiplier / Size) / > Liquidation Price = (Entry Price ± Margin / Contract Multiplier / Size) /
[ 1 ± (Maintenance Margin Ratio + Taker Rate)] [ 1 ± (Maintenance Margin Ratio + Taker Rate)]
Wherein, "+" or "-" depends on whether the contract goes long or short: Wherein, "+" or "-" depends on whether the contract goes long or short:
@ -2758,13 +2811,14 @@ class Exchange:
:param is_short: True if the trade is a short, false otherwise :param is_short: True if the trade is a short, false otherwise
:param amount: Absolute value of position size incl. leverage (in base currency) :param amount: Absolute value of position size incl. leverage (in base currency)
:param stake_amount: Stake amount - Collateral in settle currency. :param stake_amount: Stake amount - Collateral in settle currency.
:param leverage: Leverage used for this position.
:param trading_mode: SPOT, MARGIN, FUTURES, etc. :param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param margin_mode: Either ISOLATED or CROSS :param margin_mode: Either ISOLATED or CROSS
:param wallet_balance: Amount of margin_mode in the wallet being used to trade :param wallet_balance: Amount of margin_mode in the wallet being used to trade
Cross-Margin Mode: crossWalletBalance Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance Isolated-Margin Mode: isolatedWalletBalance
# * Not required by Gateio or OKX # * Not required by Gate or OKX
:param mm_ex_1: :param mm_ex_1:
:param upnl_ex_1: :param upnl_ex_1:
""" """

View File

@ -4,7 +4,7 @@ from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from freqtrade.constants import BuySell from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, PriceType, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.misc import safe_value_fallback2 from freqtrade.misc import safe_value_fallback2
@ -13,7 +13,7 @@ from freqtrade.misc import safe_value_fallback2
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Gateio(Exchange): class Gate(Exchange):
""" """
Gate.io exchange class. Contains adjustments needed for Freqtrade to work Gate.io exchange class. Contains adjustments needed for Freqtrade to work
with this exchange. with this exchange.
@ -34,6 +34,12 @@ class Gateio(Exchange):
"needs_trading_fees": True, "needs_trading_fees": True,
"fee_cost_in_contracts": False, # Set explicitly to false for clarity "fee_cost_in_contracts": False, # Set explicitly to false for clarity
"order_props_in_contracts": ['amount', 'filled', 'remaining'], "order_props_in_contracts": ['amount', 'filled', 'remaining'],
"stop_price_type_field": "price_type",
"stop_price_type_value_mapping": {
PriceType.LAST: 0,
PriceType.MARK: 1,
PriceType.INDEX: 2,
},
} }
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
@ -49,6 +55,7 @@ class Gateio(Exchange):
if any(v == 'market' for k, v in order_types.items()): if any(v == 'market' for k, v in order_types.items()):
raise OperationalException( raise OperationalException(
f'Exchange {self.name} does not support market orders.') f'Exchange {self.name} does not support market orders.')
super().validate_stop_ordertypes(order_types)
def _get_params( def _get_params(
self, self,
@ -77,7 +84,7 @@ class Gateio(Exchange):
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
# Futures usually don't contain fees in the response. # Futures usually don't contain fees in the response.
# As such, futures orders on gateio will not contain a fee, which causes # As such, futures orders on gate will not contain a fee, which causes
# a repeated "update fee" cycle and wrong calculations. # a repeated "update fee" cycle and wrong calculations.
# Therefore we patch the response with fees if it's not available. # Therefore we patch the response with fees if it's not available.
# An alternative also contianing fees would be # An alternative also contianing fees would be

View File

@ -19,5 +19,4 @@ class Hitbtc(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"ohlcv_candle_limit": 1000, "ohlcv_candle_limit": 1000,
"ohlcv_params": {"sort": "DESC"}
} }

View File

@ -97,8 +97,8 @@ class Kraken(Exchange):
)) ))
@retrier(retries=0) @retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, def create_stoploss(self, pair: str, amount: float, stop_price: float,
order_types: Dict, side: BuySell, leverage: float) -> Dict: order_types: Dict, side: BuySell, leverage: float) -> Dict:
""" """
Creates a stoploss market order. Creates a stoploss market order.
Stoploss market orders is the only stoploss type supported by kraken. Stoploss market orders is the only stoploss type supported by kraken.
@ -158,7 +158,8 @@ class Kraken(Exchange):
self, self,
leverage: float, leverage: float,
pair: Optional[str] = None, pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False,
): ):
""" """
Kraken set's the leverage as an option in the order object, so we need to Kraken set's the leverage as an option in the order object, so we need to

View File

@ -36,3 +36,34 @@ class Kucoin(Exchange):
'stop': 'loss' 'stop': 'loss'
}) })
return params return params
def create_order(
self,
*,
pair: str,
ordertype: str,
side: BuySell,
amount: float,
rate: float,
leverage: float,
reduceOnly: bool = False,
time_in_force: str = 'GTC',
) -> Dict:
res = super().create_order(
pair=pair,
ordertype=ordertype,
side=side,
amount=amount,
rate=rate,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
# Kucoin returns only the order-id.
# ccxt returns status = 'closed' at the moment - which is information ccxt invented.
# Since we rely on status heavily, we must set it to 'open' here.
# ref: https://github.com/ccxt/ccxt/pull/16674, (https://github.com/ccxt/ccxt/pull/16553)
res['type'] = ordertype
res['status'] = 'open'
return res

View File

@ -5,6 +5,7 @@ import ccxt
from freqtrade.constants import BuySell from freqtrade.constants import BuySell
from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.enums.pricetype import PriceType
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange, date_minus_candles from freqtrade.exchange import Exchange, date_minus_candles
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
@ -27,6 +28,12 @@ class Okx(Exchange):
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"tickers_have_quoteVolume": False, "tickers_have_quoteVolume": False,
"fee_cost_in_contracts": True, "fee_cost_in_contracts": True,
"stop_price_type_field": "tpTriggerPxType",
"stop_price_type_value_mapping": {
PriceType.LAST: "last",
PriceType.MARK: "index",
PriceType.INDEX: "mark",
},
} }
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
@ -118,13 +125,15 @@ class Okx(Exchange):
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None: if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
try: try:
# TODO-lev: Test me properly (check mgnMode passed) # TODO-lev: Test me properly (check mgnMode passed)
self._api.set_leverage( res = self._api.set_leverage(
leverage=leverage, leverage=leverage,
symbol=pair, symbol=pair,
params={ params={
"mgnMode": self.margin_mode.value, "mgnMode": self.margin_mode.value,
"posSide": self._get_posSide(side, False), "posSide": self._get_posSide(side, False),
}) })
self._log_exchange_response('set_leverage', res)
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:

View File

@ -15,6 +15,15 @@ class Ticker(TypedDict):
# Several more - only listing required. # Several more - only listing required.
class OrderBook(TypedDict):
symbol: str
bids: List[Tuple[float, float]]
asks: List[Tuple[float, float]]
timestamp: Optional[int]
datetime: Optional[str]
nonce: Optional[int]
Tickers = Dict[str, Ticker] Tickers = Dict[str, Ticker]
# pair, timeframe, candleType, OHLCV, drop last?, # pair, timeframe, candleType, OHLCV, drop last?,

View File

@ -45,7 +45,8 @@ class BaseEnvironment(gym.Env):
def __init__(self, df: DataFrame = DataFrame(), prices: DataFrame = DataFrame(), def __init__(self, df: DataFrame = DataFrame(), prices: DataFrame = DataFrame(),
reward_kwargs: dict = {}, window_size=10, starting_point=True, reward_kwargs: dict = {}, window_size=10, starting_point=True,
id: str = 'baseenv-1', seed: int = 1, config: dict = {}, live: bool = False, id: str = 'baseenv-1', seed: int = 1, config: dict = {}, live: bool = False,
fee: float = 0.0015, can_short: bool = False): fee: float = 0.0015, can_short: bool = False, pair: str = "",
df_raw: DataFrame = DataFrame()):
""" """
Initializes the training/eval environment. Initializes the training/eval environment.
:param df: dataframe of features :param df: dataframe of features
@ -60,12 +61,14 @@ class BaseEnvironment(gym.Env):
:param fee: The fee to use for environmental interactions. :param fee: The fee to use for environmental interactions.
:param can_short: Whether or not the environment can short :param can_short: Whether or not the environment can short
""" """
self.config = config self.config: dict = config
self.rl_config = config['freqai']['rl_config'] self.rl_config: dict = config['freqai']['rl_config']
self.add_state_info = self.rl_config.get('add_state_info', False) self.add_state_info: bool = self.rl_config.get('add_state_info', False)
self.id = id self.id: str = id
self.max_drawdown = 1 - self.rl_config.get('max_training_drawdown_pct', 0.8) self.max_drawdown: float = 1 - self.rl_config.get('max_training_drawdown_pct', 0.8)
self.compound_trades = config['stake_amount'] == 'unlimited' self.compound_trades: bool = config['stake_amount'] == 'unlimited'
self.pair: str = pair
self.raw_features: DataFrame = df_raw
if self.config.get('fee', None) is not None: if self.config.get('fee', None) is not None:
self.fee = self.config['fee'] self.fee = self.config['fee']
else: else:
@ -74,8 +77,8 @@ class BaseEnvironment(gym.Env):
# set here to default 5Ac, but all children envs can override this # set here to default 5Ac, but all children envs can override this
self.actions: Type[Enum] = BaseActions self.actions: Type[Enum] = BaseActions
self.tensorboard_metrics: dict = {} self.tensorboard_metrics: dict = {}
self.can_short = can_short self.can_short: bool = can_short
self.live = live self.live: bool = live
if not self.live and self.add_state_info: if not self.live and self.add_state_info:
self.add_state_info = False self.add_state_info = False
logger.warning("add_state_info is not available in backtesting. Deactivating.") logger.warning("add_state_info is not available in backtesting. Deactivating.")
@ -93,13 +96,12 @@ class BaseEnvironment(gym.Env):
:param reward_kwargs: extra config settings assigned by user in `rl_config` :param reward_kwargs: extra config settings assigned by user in `rl_config`
:param starting_point: start at edge of window or not :param starting_point: start at edge of window or not
""" """
self.df = df self.signal_features: DataFrame = df
self.signal_features = self.df self.prices: DataFrame = prices
self.prices = prices self.window_size: int = window_size
self.window_size = window_size self.starting_point: bool = starting_point
self.starting_point = starting_point self.rr: float = reward_kwargs["rr"]
self.rr = reward_kwargs["rr"] self.profit_aim: float = reward_kwargs["profit_aim"]
self.profit_aim = reward_kwargs["profit_aim"]
# # spaces # # spaces
if self.add_state_info: if self.add_state_info:

View File

@ -1,3 +1,4 @@
import copy
import importlib import importlib
import logging import logging
from abc import abstractmethod from abc import abstractmethod
@ -50,6 +51,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
self.eval_callback: Optional[EvalCallback] = None self.eval_callback: Optional[EvalCallback] = None
self.model_type = self.freqai_info['rl_config']['model_type'] self.model_type = self.freqai_info['rl_config']['model_type']
self.rl_config = self.freqai_info['rl_config'] self.rl_config = self.freqai_info['rl_config']
self.df_raw: DataFrame = DataFrame()
self.continual_learning = self.freqai_info.get('continual_learning', False) self.continual_learning = self.freqai_info.get('continual_learning', False)
if self.model_type in SB3_MODELS: if self.model_type in SB3_MODELS:
import_str = 'stable_baselines3' import_str = 'stable_baselines3'
@ -107,6 +109,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
data_dictionary: Dict[str, Any] = dk.make_train_test_datasets( data_dictionary: Dict[str, Any] = dk.make_train_test_datasets(
features_filtered, labels_filtered) features_filtered, labels_filtered)
self.df_raw = copy.deepcopy(data_dictionary["train_features"])
dk.fit_labels() # FIXME useless for now, but just satiating append methods dk.fit_labels() # FIXME useless for now, but just satiating append methods
# normalize all data based on train_dataset only # normalize all data based on train_dataset only
@ -143,7 +146,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
train_df = data_dictionary["train_features"] train_df = data_dictionary["train_features"]
test_df = data_dictionary["test_features"] test_df = data_dictionary["test_features"]
env_info = self.pack_env_dict() env_info = self.pack_env_dict(dk.pair)
self.train_env = self.MyRLEnv(df=train_df, self.train_env = self.MyRLEnv(df=train_df,
prices=prices_train, prices=prices_train,
@ -158,7 +161,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
actions = self.train_env.get_actions() actions = self.train_env.get_actions()
self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions) self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions)
def pack_env_dict(self) -> Dict[str, Any]: def pack_env_dict(self, pair: str) -> Dict[str, Any]:
""" """
Create dictionary of environment arguments Create dictionary of environment arguments
""" """
@ -166,7 +169,9 @@ class BaseReinforcementLearningModel(IFreqaiModel):
"reward_kwargs": self.reward_params, "reward_kwargs": self.reward_params,
"config": self.config, "config": self.config,
"live": self.live, "live": self.live,
"can_short": self.can_short} "can_short": self.can_short,
"pair": pair,
"df_raw": self.df_raw}
if self.data_provider: if self.data_provider:
env_info["fee"] = self.data_provider._exchange \ env_info["fee"] = self.data_provider._exchange \
.get_fee(symbol=self.data_provider.current_whitelist()[0]) # type: ignore .get_fee(symbol=self.data_provider.current_whitelist()[0]) # type: ignore
@ -347,7 +352,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
sets a custom reward based on profit and trade duration. sets a custom reward based on profit and trade duration.
""" """
def calculate_reward(self, action: int) -> float: def calculate_reward(self, action: int) -> float: # noqa: C901
""" """
An example reward function. This is the one function that users will likely An example reward function. This is the one function that users will likely
wish to inject their own creativity into. wish to inject their own creativity into.
@ -363,10 +368,19 @@ class BaseReinforcementLearningModel(IFreqaiModel):
pnl = self.get_unrealized_profit() pnl = self.get_unrealized_profit()
factor = 100. factor = 100.
# you can use feature values from dataframe
rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{self.pair}_"
f"{self.config['timeframe']}"].iloc[self._current_tick]
# reward agent for entering trades # reward agent for entering trades
if (action in (Actions.Long_enter.value, Actions.Short_enter.value) if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
and self._position == Positions.Neutral): and self._position == Positions.Neutral):
return 25 if rsi_now < 40:
factor = 40 / rsi_now
else:
factor = 1
return 25 * factor
# discourage agent from not entering trades # discourage agent from not entering trades
if action == Actions.Neutral.value and self._position == Positions.Neutral: if action == Actions.Neutral.value and self._position == Positions.Neutral:
return -1 return -1

View File

@ -59,7 +59,7 @@ class FreqaiDataDrawer:
Juha Nykänen @suikula, Wagner Costa @wagnercosta, Johan Vlugt @Jooopieeert Juha Nykänen @suikula, Wagner Costa @wagnercosta, Johan Vlugt @Jooopieeert
""" """
def __init__(self, full_path: Path, config: Config, follow_mode: bool = False): def __init__(self, full_path: Path, config: Config):
self.config = config self.config = config
self.freqai_info = config.get("freqai", {}) self.freqai_info = config.get("freqai", {})
@ -72,21 +72,13 @@ class FreqaiDataDrawer:
self.model_return_values: Dict[str, DataFrame] = {} self.model_return_values: Dict[str, DataFrame] = {}
self.historic_data: Dict[str, Dict[str, DataFrame]] = {} self.historic_data: Dict[str, Dict[str, DataFrame]] = {}
self.historic_predictions: Dict[str, DataFrame] = {} self.historic_predictions: Dict[str, DataFrame] = {}
self.follower_dict: Dict[str, pair_info] = {}
self.full_path = full_path self.full_path = full_path
self.follower_name: str = self.config.get("bot_name", "follower1")
self.follower_dict_path = Path(
self.full_path / f"follower_dictionary-{self.follower_name}.json"
)
self.historic_predictions_path = Path(self.full_path / "historic_predictions.pkl") self.historic_predictions_path = Path(self.full_path / "historic_predictions.pkl")
self.historic_predictions_bkp_path = Path( self.historic_predictions_bkp_path = Path(
self.full_path / "historic_predictions.backup.pkl") self.full_path / "historic_predictions.backup.pkl")
self.pair_dictionary_path = Path(self.full_path / "pair_dictionary.json") self.pair_dictionary_path = Path(self.full_path / "pair_dictionary.json")
self.global_metadata_path = Path(self.full_path / "global_metadata.json") self.global_metadata_path = Path(self.full_path / "global_metadata.json")
self.metric_tracker_path = Path(self.full_path / "metric_tracker.json") self.metric_tracker_path = Path(self.full_path / "metric_tracker.json")
self.follow_mode = follow_mode
if follow_mode:
self.create_follower_dict()
self.load_drawer_from_disk() self.load_drawer_from_disk()
self.load_historic_predictions_from_disk() self.load_historic_predictions_from_disk()
self.metric_tracker: Dict[str, Dict[str, Dict[str, list]]] = {} self.metric_tracker: Dict[str, Dict[str, Dict[str, list]]] = {}
@ -149,13 +141,8 @@ class FreqaiDataDrawer:
if exists: if exists:
with open(self.pair_dictionary_path, "r") as fp: with open(self.pair_dictionary_path, "r") as fp:
self.pair_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) self.pair_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
elif not self.follow_mode:
logger.info("Could not find existing datadrawer, starting from scratch")
else: else:
logger.warning( logger.info("Could not find existing datadrawer, starting from scratch")
f"Follower could not find pair_dictionary at {self.full_path} "
"sending null values back to strategy"
)
def load_metric_tracker_from_disk(self): def load_metric_tracker_from_disk(self):
""" """
@ -193,13 +180,8 @@ class FreqaiDataDrawer:
self.historic_predictions = cloudpickle.load(fp) self.historic_predictions = cloudpickle.load(fp)
logger.warning('FreqAI successfully loaded the backup historical predictions file.') logger.warning('FreqAI successfully loaded the backup historical predictions file.')
elif not self.follow_mode:
logger.info("Could not find existing historic_predictions, starting from scratch")
else: else:
logger.warning( logger.info("Could not find existing historic_predictions, starting from scratch")
f"Follower could not find historic predictions at {self.full_path} "
"sending null values back to strategy"
)
return exists return exists
@ -231,14 +213,6 @@ class FreqaiDataDrawer:
rapidjson.dump(self.pair_dict, fp, default=self.np_encoder, rapidjson.dump(self.pair_dict, fp, default=self.np_encoder,
number_mode=rapidjson.NM_NATIVE) number_mode=rapidjson.NM_NATIVE)
def save_follower_dict_to_disk(self):
"""
Save follower dictionary to disk (used by strategy for persistent prediction targets)
"""
with open(self.follower_dict_path, "w") as fp:
rapidjson.dump(self.follower_dict, fp, default=self.np_encoder,
number_mode=rapidjson.NM_NATIVE)
def save_global_metadata_to_disk(self, metadata: Dict[str, Any]): def save_global_metadata_to_disk(self, metadata: Dict[str, Any]):
""" """
Save global metadata json to disk Save global metadata json to disk
@ -248,28 +222,11 @@ class FreqaiDataDrawer:
rapidjson.dump(metadata, fp, default=self.np_encoder, rapidjson.dump(metadata, fp, default=self.np_encoder,
number_mode=rapidjson.NM_NATIVE) number_mode=rapidjson.NM_NATIVE)
def create_follower_dict(self):
"""
Create or dictionary for each follower to maintain unique persistent prediction targets
"""
whitelist_pairs = self.config.get("exchange", {}).get("pair_whitelist")
exists = self.follower_dict_path.is_file()
if exists:
logger.info("Found an existing follower dictionary")
for pair in whitelist_pairs:
self.follower_dict[pair] = {}
self.save_follower_dict_to_disk()
def np_encoder(self, object): def np_encoder(self, object):
if isinstance(object, np.generic): if isinstance(object, np.generic):
return object.item() return object.item()
def get_pair_dict_info(self, pair: str) -> Tuple[str, int, bool]: def get_pair_dict_info(self, pair: str) -> Tuple[str, int]:
""" """
Locate and load existing model metadata from persistent storage. If not located, Locate and load existing model metadata from persistent storage. If not located,
create a new one and append the current pair to it and prepare it for its first create a new one and append the current pair to it and prepare it for its first
@ -278,32 +235,19 @@ class FreqaiDataDrawer:
:return: :return:
model_filename: str = unique filename used for loading persistent objects from disk model_filename: str = unique filename used for loading persistent objects from disk
trained_timestamp: int = the last time the coin was trained trained_timestamp: int = the last time the coin was trained
return_null_array: bool = Follower could not find pair metadata
""" """
pair_dict = self.pair_dict.get(pair) pair_dict = self.pair_dict.get(pair)
data_path_set = self.pair_dict.get(pair, self.empty_pair_dict).get("data_path", "")
return_null_array = False
if pair_dict: if pair_dict:
model_filename = pair_dict["model_filename"] model_filename = pair_dict["model_filename"]
trained_timestamp = pair_dict["trained_timestamp"] trained_timestamp = pair_dict["trained_timestamp"]
elif not self.follow_mode: else:
self.pair_dict[pair] = self.empty_pair_dict.copy() self.pair_dict[pair] = self.empty_pair_dict.copy()
model_filename = "" model_filename = ""
trained_timestamp = 0 trained_timestamp = 0
if not data_path_set and self.follow_mode: return model_filename, trained_timestamp
logger.warning(
f"Follower could not find current pair {pair} in "
f"pair_dictionary at path {self.full_path}, sending null values "
"back to strategy."
)
trained_timestamp = 0
model_filename = ''
return_null_array = True
return model_filename, trained_timestamp, return_null_array
def set_pair_dict_info(self, metadata: dict) -> None: def set_pair_dict_info(self, metadata: dict) -> None:
pair_in_dict = self.pair_dict.get(metadata["pair"]) pair_in_dict = self.pair_dict.get(metadata["pair"])
@ -311,7 +255,6 @@ class FreqaiDataDrawer:
return return
else: else:
self.pair_dict[metadata["pair"]] = self.empty_pair_dict.copy() self.pair_dict[metadata["pair"]] = self.empty_pair_dict.copy()
return return
def set_initial_return_values(self, pair: str, pred_df: DataFrame) -> None: def set_initial_return_values(self, pair: str, pred_df: DataFrame) -> None:
@ -423,6 +366,12 @@ class FreqaiDataDrawer:
def purge_old_models(self) -> None: def purge_old_models(self) -> None:
num_keep = self.freqai_info["purge_old_models"]
if not num_keep:
return
elif type(num_keep) == bool:
num_keep = 2
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()] model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
pattern = re.compile(r"sub-train-(\w+)_(\d{10})") pattern = re.compile(r"sub-train-(\w+)_(\d{10})")
@ -445,11 +394,11 @@ class FreqaiDataDrawer:
delete_dict[coin]["timestamps"][int(timestamp)] = dir delete_dict[coin]["timestamps"][int(timestamp)] = dir
for coin in delete_dict: for coin in delete_dict:
if delete_dict[coin]["num_folders"] > 2: if delete_dict[coin]["num_folders"] > num_keep:
sorted_dict = collections.OrderedDict( sorted_dict = collections.OrderedDict(
sorted(delete_dict[coin]["timestamps"].items()) sorted(delete_dict[coin]["timestamps"].items())
) )
num_delete = len(sorted_dict) - 2 num_delete = len(sorted_dict) - num_keep
deleted = 0 deleted = 0
for k, v in sorted_dict.items(): for k, v in sorted_dict.items():
if deleted >= num_delete: if deleted >= num_delete:
@ -458,12 +407,6 @@ class FreqaiDataDrawer:
shutil.rmtree(v) shutil.rmtree(v)
deleted += 1 deleted += 1
def update_follower_metadata(self):
# follower needs to load from disk to get any changes made by leader to pair_dict
self.load_drawer_from_disk()
if self.config.get("freqai", {}).get("purge_old_models", False):
self.purge_old_models()
def save_metadata(self, dk: FreqaiDataKitchen) -> None: def save_metadata(self, dk: FreqaiDataKitchen) -> None:
""" """
Saves only metadata for backtesting studies if user prefers Saves only metadata for backtesting studies if user prefers

View File

@ -1,6 +1,7 @@
import copy import copy
import inspect import inspect
import logging import logging
import random
import shutil import shutil
from datetime import datetime, timezone from datetime import datetime, timezone
from math import cos, sin from math import cos, sin
@ -170,6 +171,19 @@ class FreqaiDataKitchen:
train_labels = labels train_labels = labels
train_weights = weights train_weights = weights
if feat_dict["shuffle_after_split"]:
rint1 = random.randint(0, 100)
rint2 = random.randint(0, 100)
train_features = train_features.sample(
frac=1, random_state=rint1).reset_index(drop=True)
train_labels = train_labels.sample(frac=1, random_state=rint1).reset_index(drop=True)
train_weights = pd.DataFrame(train_weights).sample(
frac=1, random_state=rint1).reset_index(drop=True).to_numpy()[:, 0]
test_features = test_features.sample(frac=1, random_state=rint2).reset_index(drop=True)
test_labels = test_labels.sample(frac=1, random_state=rint2).reset_index(drop=True)
test_weights = pd.DataFrame(test_weights).sample(
frac=1, random_state=rint2).reset_index(drop=True).to_numpy()[:, 0]
# Simplest way to reverse the order of training and test data: # Simplest way to reverse the order of training and test data:
if self.freqai_config['feature_parameters'].get('reverse_train_test_order', False): if self.freqai_config['feature_parameters'].get('reverse_train_test_order', False):
return self.build_data_dictionary( return self.build_data_dictionary(
@ -1247,17 +1261,19 @@ class FreqaiDataKitchen:
tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes") tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes")
for tf in tfs: for tf in tfs:
metadata = {"pair": pair, "tf": tf}
informative_df = self.get_pair_data_for_features( informative_df = self.get_pair_data_for_features(
pair, tf, strategy, corr_dataframes, base_dataframes, is_corr_pairs) pair, tf, strategy, corr_dataframes, base_dataframes, is_corr_pairs)
informative_copy = informative_df.copy() informative_copy = informative_df.copy()
for t in self.freqai_config["feature_parameters"]["indicator_periods_candles"]: for t in self.freqai_config["feature_parameters"]["indicator_periods_candles"]:
df_features = strategy.feature_engineering_expand_all( df_features = strategy.feature_engineering_expand_all(
informative_copy.copy(), t) informative_copy.copy(), t, metadata=metadata)
suffix = f"{t}" suffix = f"{t}"
informative_df = self.merge_features(informative_df, df_features, tf, tf, suffix) informative_df = self.merge_features(informative_df, df_features, tf, tf, suffix)
generic_df = strategy.feature_engineering_expand_basic(informative_copy.copy()) generic_df = strategy.feature_engineering_expand_basic(
informative_copy.copy(), metadata=metadata)
suffix = "gen" suffix = "gen"
informative_df = self.merge_features(informative_df, generic_df, tf, tf, suffix) informative_df = self.merge_features(informative_df, generic_df, tf, tf, suffix)
@ -1326,8 +1342,8 @@ class FreqaiDataKitchen:
"include_corr_pairlist", []) "include_corr_pairlist", [])
dataframe = self.populate_features(dataframe.copy(), pair, strategy, dataframe = self.populate_features(dataframe.copy(), pair, strategy,
corr_dataframes, base_dataframes) corr_dataframes, base_dataframes)
metadata = {"pair": pair}
dataframe = strategy.feature_engineering_standard(dataframe.copy()) dataframe = strategy.feature_engineering_standard(dataframe.copy(), metadata=metadata)
# ensure corr pairs are always last # ensure corr pairs are always last
for corr_pair in corr_pairs: for corr_pair in corr_pairs:
if pair == corr_pair: if pair == corr_pair:
@ -1336,7 +1352,7 @@ class FreqaiDataKitchen:
dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy, dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy,
corr_dataframes, base_dataframes, True) corr_dataframes, base_dataframes, True)
dataframe = strategy.set_freqai_targets(dataframe.copy()) dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata)
self.get_unique_classes_from_labels(dataframe) self.get_unique_classes_from_labels(dataframe)
@ -1546,3 +1562,25 @@ class FreqaiDataKitchen:
dataframe.columns = dataframe.columns.str.replace(c, "") dataframe.columns = dataframe.columns.str.replace(c, "")
return dataframe return dataframe
def buffer_timerange(self, timerange: TimeRange):
"""
Buffer the start and end of the timerange. This is used *after* the indicators
are populated.
The main example use is when predicting maxima and minima, the argrelextrema
function cannot know the maxima/minima at the edges of the timerange. To improve
model accuracy, it is best to compute argrelextrema on the full timerange
and then use this function to cut off the edges (buffer) by the kernel.
In another case, if the targets are set to a shifted price movement, this
buffer is unnecessary because the shifted candles at the end of the timerange
will be NaN and FreqAI will automatically cut those off of the training
dataset.
"""
buffer = self.freqai_config["feature_parameters"]["buffer_train_data_candles"]
if buffer:
timerange.stopts -= buffer * timeframe_to_seconds(self.config["timeframe"])
timerange.startts += buffer * timeframe_to_seconds(self.config["timeframe"])
return timerange

View File

@ -66,12 +66,11 @@ class IFreqaiModel(ABC):
self.retrain = False self.retrain = False
self.first = True self.first = True
self.set_full_path() self.set_full_path()
self.follow_mode: bool = self.freqai_info.get("follow_mode", False)
self.save_backtest_models: bool = self.freqai_info.get("save_backtest_models", True) self.save_backtest_models: bool = self.freqai_info.get("save_backtest_models", True)
if self.save_backtest_models: if self.save_backtest_models:
logger.info('Backtesting module configured to save all models.') logger.info('Backtesting module configured to save all models.')
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode) self.dd = FreqaiDataDrawer(Path(self.full_path), self.config)
# set current candle to arbitrary historical date # set current candle to arbitrary historical date
self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=timezone.utc) self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=timezone.utc)
self.dd.current_candle = self.current_candle self.dd.current_candle = self.current_candle
@ -153,7 +152,7 @@ class IFreqaiModel(ABC):
# (backtest window, i.e. window immediately following the training window). # (backtest window, i.e. window immediately following the training window).
# FreqAI slides the window and sequentially builds the backtesting results before returning # FreqAI slides the window and sequentially builds the backtesting results before returning
# the concatenated results for the full backtesting period back to the strategy. # the concatenated results for the full backtesting period back to the strategy.
elif not self.follow_mode: else:
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"]) self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
if not self.config.get("freqai_backtest_live_models", False): if not self.config.get("freqai_backtest_live_models", False):
logger.info(f"Training {len(self.dk.training_timeranges)} timeranges") logger.info(f"Training {len(self.dk.training_timeranges)} timeranges")
@ -228,7 +227,7 @@ class IFreqaiModel(ABC):
logger.warning(f'{pair} not in current whitelist, removing from train queue.') logger.warning(f'{pair} not in current whitelist, removing from train queue.')
continue continue
(_, trained_timestamp, _) = self.dd.get_pair_dict_info(pair) (_, trained_timestamp) = self.dd.get_pair_dict_info(pair)
dk = FreqaiDataKitchen(self.config, self.live, pair) dk = FreqaiDataKitchen(self.config, self.live, pair)
( (
@ -286,7 +285,7 @@ class IFreqaiModel(ABC):
# following tr_train. Both of these windows slide through the # following tr_train. Both of these windows slide through the
# entire backtest # entire backtest
for tr_train, tr_backtest in zip(dk.training_timeranges, dk.backtesting_timeranges): for tr_train, tr_backtest in zip(dk.training_timeranges, dk.backtesting_timeranges):
(_, _, _) = self.dd.get_pair_dict_info(pair) (_, _) = self.dd.get_pair_dict_info(pair)
train_it += 1 train_it += 1
total_trains = len(dk.backtesting_timeranges) total_trains = len(dk.backtesting_timeranges)
self.training_timerange = tr_train self.training_timerange = tr_train
@ -325,9 +324,13 @@ class IFreqaiModel(ABC):
populate_indicators = False populate_indicators = False
dataframe_base_train = dataframe.loc[dataframe["date"] < tr_train.stopdt, :] dataframe_base_train = dataframe.loc[dataframe["date"] < tr_train.stopdt, :]
dataframe_base_train = strategy.set_freqai_targets(dataframe_base_train) dataframe_base_train = strategy.set_freqai_targets(
dataframe_base_train, metadata=metadata)
dataframe_base_backtest = dataframe.loc[dataframe["date"] < tr_backtest.stopdt, :] dataframe_base_backtest = dataframe.loc[dataframe["date"] < tr_backtest.stopdt, :]
dataframe_base_backtest = strategy.set_freqai_targets(dataframe_base_backtest) dataframe_base_backtest = strategy.set_freqai_targets(
dataframe_base_backtest, metadata=metadata)
tr_train = dk.buffer_timerange(tr_train)
dataframe_train = dk.slice_dataframe(tr_train, dataframe_base_train) dataframe_train = dk.slice_dataframe(tr_train, dataframe_base_train)
dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe_base_backtest) dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe_base_backtest)
@ -379,18 +382,9 @@ class IFreqaiModel(ABC):
:returns: :returns:
dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only
""" """
# update follower
if self.follow_mode:
self.dd.update_follower_metadata()
# get the model metadata associated with the current pair # get the model metadata associated with the current pair
(_, trained_timestamp, return_null_array) = self.dd.get_pair_dict_info(metadata["pair"]) (_, trained_timestamp) = self.dd.get_pair_dict_info(metadata["pair"])
# if the metadata doesn't exist, the follower returns null arrays to strategy
if self.follow_mode and return_null_array:
logger.info("Returning null array from follower to strategy")
self.dd.return_null_values_to_strategy(dataframe, dk)
return dk
# append the historic data once per round # append the historic data once per round
if self.dd.historic_data: if self.dd.historic_data:
@ -398,27 +392,18 @@ class IFreqaiModel(ABC):
logger.debug(f'Updating historic data on pair {metadata["pair"]}') logger.debug(f'Updating historic data on pair {metadata["pair"]}')
self.track_current_candle() self.track_current_candle()
if not self.follow_mode: (_, new_trained_timerange, data_load_timerange) = dk.check_if_new_training_required(
trained_timestamp
)
dk.set_paths(metadata["pair"], new_trained_timerange.stopts)
(_, new_trained_timerange, data_load_timerange) = dk.check_if_new_training_required( # load candle history into memory if it is not yet.
trained_timestamp if not self.dd.historic_data:
) self.dd.load_all_pair_histories(data_load_timerange, dk)
dk.set_paths(metadata["pair"], new_trained_timerange.stopts)
# load candle history into memory if it is not yet. if not self.scanning:
if not self.dd.historic_data: self.scanning = True
self.dd.load_all_pair_histories(data_load_timerange, dk) self.start_scanning(strategy)
if not self.scanning:
self.scanning = True
self.start_scanning(strategy)
elif self.follow_mode:
dk.set_paths(metadata["pair"], trained_timestamp)
logger.info(
"FreqAI instance set to follow_mode, finding existing pair "
f"using { self.identifier }"
)
# load the model and associated data into the data kitchen # load the model and associated data into the data kitchen
self.model = self.dd.load_data(metadata["pair"], dk) self.model = self.dd.load_data(metadata["pair"], dk)
@ -580,7 +565,13 @@ class IFreqaiModel(ABC):
:return: :return:
:boolean: whether the model file exists or not. :boolean: whether the model file exists or not.
""" """
path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model.joblib") if self.dd.model_type == 'joblib':
file_type = ".joblib"
elif self.dd.model_type == 'keras':
file_type = ".h5"
elif 'stable_baselines' in self.dd.model_type or 'sb3_contrib' == self.dd.model_type:
file_type = ".zip"
path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}")
file_exists = path_to_modelfile.is_file() file_exists = path_to_modelfile.is_file()
if file_exists: if file_exists:
logger.info("Found model at %s", dk.data_path / dk.model_filename) logger.info("Found model at %s", dk.data_path / dk.model_filename)
@ -625,6 +616,8 @@ class IFreqaiModel(ABC):
strategy, corr_dataframes, base_dataframes, pair strategy, corr_dataframes, base_dataframes, pair
) )
new_trained_timerange = dk.buffer_timerange(new_trained_timerange)
unfiltered_dataframe = dk.slice_dataframe(new_trained_timerange, unfiltered_dataframe) unfiltered_dataframe = dk.slice_dataframe(new_trained_timerange, unfiltered_dataframe)
# find the features indicated by strategy and store in datakitchen # find the features indicated by strategy and store in datakitchen
@ -640,8 +633,7 @@ class IFreqaiModel(ABC):
if self.plot_features: if self.plot_features:
plot_feature_importance(model, pair, dk, self.plot_features) plot_feature_importance(model, pair, dk, self.plot_features)
if self.freqai_info.get("purge_old_models", False): self.dd.purge_old_models()
self.dd.purge_old_models()
def set_initial_historic_predictions( def set_initial_historic_predictions(
self, pred_df: DataFrame, dk: FreqaiDataKitchen, pair: str, strat_df: DataFrame self, pred_df: DataFrame, dk: FreqaiDataKitchen, pair: str, strat_df: DataFrame

View File

@ -34,7 +34,7 @@ class ReinforcementLearner_multiproc(ReinforcementLearner):
train_df = data_dictionary["train_features"] train_df = data_dictionary["train_features"]
test_df = data_dictionary["test_features"] test_df = data_dictionary["test_features"]
env_info = self.pack_env_dict() env_info = self.pack_env_dict(dk.pair)
env_id = "train_env" env_id = "train_env"
self.train_env = SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1, self.train_env = SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1,

View File

@ -344,7 +344,15 @@ class FreqtradeBot(LoggingMixin):
try: try:
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
order.ft_order_side == 'stoploss') order.ft_order_side == 'stoploss')
if not order.trade:
# This should not happen, but it does if trades were deleted manually.
# This can only incur on sqlite, which doesn't enforce foreign constraints.
logger.warning(
f"Order {order.order_id} has no trade attached. "
"This may suggest a database corruption. "
f"The expected trade ID is {order.ft_trade_id}. Ignoring this order."
)
continue
self.update_trade_state(order.trade, order.order_id, fo, self.update_trade_state(order.trade, order.order_id, fo,
stoploss_order=(order.ft_order_side == 'stoploss')) stoploss_order=(order.ft_order_side == 'stoploss'))
@ -355,7 +363,7 @@ class FreqtradeBot(LoggingMixin):
"Order is older than 5 days. Assuming order was fully cancelled.") "Order is older than 5 days. Assuming order was fully cancelled.")
fo = order.to_ccxt_object() fo = order.to_ccxt_object()
fo['status'] = 'canceled' fo['status'] = 'canceled'
self.handle_timedout_order(fo, order.trade) self.handle_cancel_order(fo, order.trade, constants.CANCEL_REASON['TIMEOUT'])
except ExchangeError as e: except ExchangeError as e:
@ -750,13 +758,15 @@ class FreqtradeBot(LoggingMixin):
self.exchange.name, order['filled'], order['amount'], self.exchange.name, order['filled'], order['amount'],
order['remaining'] order['remaining']
) )
amount = safe_value_fallback(order, 'filled', 'amount') amount = safe_value_fallback(order, 'filled', 'amount', amount)
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') enter_limit_filled_price = safe_value_fallback(
order, 'average', 'price', enter_limit_filled_price)
# in case of FOK the order may be filled immediately and fully # in case of FOK the order may be filled immediately and fully
elif order_status == 'closed': elif order_status == 'closed':
amount = safe_value_fallback(order, 'filled', 'amount') amount = safe_value_fallback(order, 'filled', 'amount', amount)
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') enter_limit_filled_price = safe_value_fallback(
order, 'average', 'price', enter_limit_requested)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
@ -1068,7 +1078,7 @@ class FreqtradeBot(LoggingMixin):
datetime.now(timezone.utc), datetime.now(timezone.utc),
enter=enter, enter=enter,
exit_=exit_, exit_=exit_,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 force_stoploss=self.edge.get_stoploss(trade.pair) if self.edge else 0
) )
for should_exit in exits: for should_exit in exits:
if should_exit.exit_flag: if should_exit.exit_flag:
@ -1088,7 +1098,7 @@ class FreqtradeBot(LoggingMixin):
:return: True if the order succeeded, and False in case of problems. :return: True if the order succeeded, and False in case of problems.
""" """
try: try:
stoploss_order = self.exchange.stoploss( stoploss_order = self.exchange.create_stoploss(
pair=trade.pair, pair=trade.pair,
amount=trade.amount, amount=trade.amount,
stop_price=stop_price, stop_price=stop_price,
@ -1160,15 +1170,13 @@ class FreqtradeBot(LoggingMixin):
# If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
if not stoploss_order: if not stoploss_order:
stoploss = ( stop_price = trade.stoploss_or_liquidation
self.edge.stoploss(pair=trade.pair) if self.edge:
if self.edge else stoploss = self.edge.get_stoploss(pair=trade.pair)
trade.stop_loss_pct / trade.leverage stop_price = (
) trade.open_rate * (1 - stoploss) if trade.is_short
if trade.is_short: else trade.open_rate * (1 + stoploss)
stop_price = trade.open_rate * (1 - stoploss) )
else:
stop_price = trade.open_rate * (1 + stoploss)
if self.create_stoploss_order(trade=trade, stop_price=stop_price): if self.create_stoploss_order(trade=trade, stop_price=stop_price):
# The above will return False if the placement failed and the trade was force-sold. # The above will return False if the placement failed and the trade was force-sold.
@ -1253,11 +1261,11 @@ class FreqtradeBot(LoggingMixin):
if not_closed: if not_closed:
if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out( if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
trade, order_obj, datetime.now(timezone.utc))): trade, order_obj, datetime.now(timezone.utc))):
self.handle_timedout_order(order, trade) self.handle_cancel_order(order, trade, constants.CANCEL_REASON['TIMEOUT'])
else: else:
self.replace_order(order, order_obj, trade) self.replace_order(order, order_obj, trade)
def handle_timedout_order(self, order: Dict, trade: Trade) -> None: def handle_cancel_order(self, order: Dict, trade: Trade, reason: str) -> None:
""" """
Check if current analyzed order timed out and cancel if necessary. Check if current analyzed order timed out and cancel if necessary.
:param order: Order dict grabbed with exchange.fetch_order() :param order: Order dict grabbed with exchange.fetch_order()
@ -1265,10 +1273,10 @@ class FreqtradeBot(LoggingMixin):
:return: None :return: None
""" """
if order['side'] == trade.entry_side: if order['side'] == trade.entry_side:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_enter(trade, order, reason)
else: else:
canceled = self.handle_cancel_exit( canceled = self.handle_cancel_exit(
trade, order, constants.CANCEL_REASON['TIMEOUT']) trade, order, reason)
canceled_count = trade.get_exit_order_count() canceled_count = trade.get_exit_order_count()
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
@ -1626,7 +1634,7 @@ class FreqtradeBot(LoggingMixin):
# second condition is for mypy only; order will always be passed during sub trade # second condition is for mypy only; order will always be passed during sub trade
if sub_trade and order is not None: if sub_trade and order is not None:
amount = order.safe_filled if fill else order.amount amount = order.safe_filled if fill else order.safe_amount
order_rate: float = order.safe_price order_rate: float = order.safe_price
profit = trade.calc_profit(rate=order_rate, amount=amount, open_rate=trade.open_rate) profit = trade.calc_profit(rate=order_rate, amount=amount, open_rate=trade.open_rate)
@ -1789,6 +1797,7 @@ class FreqtradeBot(LoggingMixin):
is_short=trade.is_short, is_short=trade.is_short,
amount=trade.amount, amount=trade.amount,
stake_amount=trade.stake_amount, stake_amount=trade.stake_amount,
leverage=trade.leverage,
wallet_balance=trade.stake_amount, wallet_balance=trade.stake_amount,
)) ))

View File

@ -1,2 +1 @@
# flake8: noqa: F401 from freqtrade.leverage.interest import interest # noqa: F401
from freqtrade.leverage.interest import interest

View File

@ -103,9 +103,9 @@ def setup_logging(config: Config) -> None:
logging.root.addHandler(handler_sl) logging.root.addHandler(handler_sl)
elif s[0] == 'journald': # pragma: no cover elif s[0] == 'journald': # pragma: no cover
try: try:
from systemd.journal import JournaldLogHandler from cysystemd.journal import JournaldLogHandler
except ImportError: except ImportError:
raise OperationalException("You need the systemd python package be installed in " raise OperationalException("You need the cysystemd python package be installed in "
"order to use logging to journald.") "order to use logging to journald.")
handler_jd = get_existing_handlers(JournaldLogHandler) handler_jd = get_existing_handlers(JournaldLogHandler)
if handler_jd: if handler_jd:

View File

@ -1,2 +1 @@
# flake8: noqa: F401 from freqtrade.mixins.logging_mixin import LoggingMixin # noqa: F401
from freqtrade.mixins.logging_mixin import LoggingMixin

View File

@ -868,6 +868,7 @@ class Backtesting:
open_rate=propose_rate, open_rate=propose_rate,
amount=amount, amount=amount,
stake_amount=trade.stake_amount, stake_amount=trade.stake_amount,
leverage=trade.leverage,
wallet_balance=trade.stake_amount, wallet_balance=trade.stake_amount,
is_short=is_short, is_short=is_short,
)) ))

View File

@ -44,7 +44,7 @@ class SharpeHyperOptLossDaily(IHyperOptLoss):
sum_daily = ( sum_daily = (
results.resample(resample_freq, on='close_date').agg( results.resample(resample_freq, on='close_date').agg(
{"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0) {"profit_ratio_after_slippage": 'sum'}).reindex(t_index).fillna(0)
) )
total_profit = sum_daily["profit_ratio_after_slippage"] - risk_free_rate total_profit = sum_daily["profit_ratio_after_slippage"] - risk_free_rate

View File

@ -46,7 +46,7 @@ class SortinoHyperOptLossDaily(IHyperOptLoss):
sum_daily = ( sum_daily = (
results.resample(resample_freq, on='close_date').agg( results.resample(resample_freq, on='close_date').agg(
{"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0) {"profit_ratio_after_slippage": 'sum'}).reindex(t_index).fillna(0)
) )
total_profit = sum_daily["profit_ratio_after_slippage"] - minimum_acceptable_return total_profit = sum_daily["profit_ratio_after_slippage"] - minimum_acceptable_return

0
freqtrade/optimize/hyperopt_tools.py Executable file → Normal file
View File

View File

@ -1,4 +1,3 @@
# flake8: noqa: F401 from skopt.space import Categorical, Dimension, Integer, Real # noqa: F401
from skopt.space import Categorical, Dimension, Integer, Real
from .decimalspace import SKDecimal from .decimalspace import SKDecimal # noqa: F401

View File

@ -21,9 +21,9 @@ class PairLock(_DECL_BASE):
side = Column(String(25), nullable=False, default="*") side = Column(String(25), nullable=False, default="*")
reason = Column(String(255), nullable=True) reason = Column(String(255), nullable=True)
# Time the pair was locked (start time) # Time the pair was locked (start time)
lock_time = Column(DateTime, nullable=False) lock_time = Column(DateTime(), nullable=False)
# Time until the pair is locked (end time) # Time until the pair is locked (end time)
lock_end_time = Column(DateTime, nullable=False, index=True) lock_end_time = Column(DateTime(), nullable=False, index=True)
active = Column(Boolean, nullable=False, default=True, index=True) active = Column(Boolean, nullable=False, default=True, index=True)

View File

@ -46,31 +46,31 @@ class Order(_DECL_BASE):
trade = relationship("Trade", back_populates="orders") trade = relationship("Trade", back_populates="orders")
# order_side can only be 'buy', 'sell' or 'stoploss' # order_side can only be 'buy', 'sell' or 'stoploss'
ft_order_side: str = Column(String(25), nullable=False) ft_order_side = Column(String(25), nullable=False)
ft_pair: str = Column(String(25), nullable=False) ft_pair = Column(String(25), nullable=False)
ft_is_open = Column(Boolean, nullable=False, default=True, index=True) ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
ft_amount = Column(Float, nullable=False) ft_amount = Column(Float(), nullable=False)
ft_price = Column(Float, nullable=False) ft_price = Column(Float(), nullable=False)
order_id: str = Column(String(255), nullable=False, index=True) order_id = Column(String(255), nullable=False, index=True)
status = Column(String(255), nullable=True) status = Column(String(255), nullable=True)
symbol = Column(String(25), nullable=True) symbol = Column(String(25), nullable=True)
order_type: str = Column(String(50), nullable=True) order_type = Column(String(50), nullable=True)
side = Column(String(25), nullable=True) side = Column(String(25), nullable=True)
price = Column(Float, nullable=True) price = Column(Float(), nullable=True)
average = Column(Float, nullable=True) average = Column(Float(), nullable=True)
amount = Column(Float, nullable=True) amount = Column(Float(), nullable=True)
filled = Column(Float, nullable=True) filled = Column(Float(), nullable=True)
remaining = Column(Float, nullable=True) remaining = Column(Float(), nullable=True)
cost = Column(Float, nullable=True) cost = Column(Float(), nullable=True)
stop_price = Column(Float, nullable=True) stop_price = Column(Float(), nullable=True)
order_date = Column(DateTime, nullable=True, default=datetime.utcnow) order_date = Column(DateTime(), nullable=True, default=datetime.utcnow)
order_filled_date = Column(DateTime, nullable=True) order_filled_date = Column(DateTime(), nullable=True)
order_update_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime(), nullable=True)
funding_fee = Column(Float, nullable=True) funding_fee = Column(Float(), nullable=True)
ft_fee_base = Column(Float, nullable=True) ft_fee_base = Column(Float(), nullable=True)
@property @property
def order_date_utc(self) -> datetime: def order_date_utc(self) -> datetime:
@ -151,7 +151,7 @@ class Order(_DECL_BASE):
self.order_update_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) -> Dict[str, Any]:
return { order = {
'id': self.order_id, 'id': self.order_id,
'symbol': self.ft_pair, 'symbol': self.ft_pair,
'price': self.price, 'price': self.price,
@ -169,10 +169,13 @@ class Order(_DECL_BASE):
'fee': None, 'fee': None,
'info': {}, 'info': {},
} }
if self.ft_order_side == 'stoploss':
order['ft_order_type'] = 'stoploss'
return order
def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]: def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
resp = { resp = {
'amount': self.amount, 'amount': self.safe_amount,
'safe_price': self.safe_price, 'safe_price': self.safe_price,
'ft_order_side': self.ft_order_side, 'ft_order_side': self.ft_order_side,
'order_filled_timestamp': int(self.order_filled_date.replace( 'order_filled_timestamp': int(self.order_filled_date.replace(
@ -1177,44 +1180,44 @@ class Trade(_DECL_BASE, LocalTrade):
base_currency = Column(String(25), nullable=True) base_currency = Column(String(25), nullable=True)
stake_currency = Column(String(25), nullable=True) stake_currency = Column(String(25), nullable=True)
is_open = Column(Boolean, nullable=False, default=True, index=True) is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0) fee_open = Column(Float(), nullable=False, default=0.0)
fee_open_cost = Column(Float, nullable=True) fee_open_cost = Column(Float(), nullable=True)
fee_open_currency = Column(String(25), nullable=True) fee_open_currency = Column(String(25), nullable=True)
fee_close = Column(Float, nullable=False, default=0.0) fee_close = Column(Float(), nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True) fee_close_cost = Column(Float(), nullable=True)
fee_close_currency = Column(String(25), nullable=True) fee_close_currency = Column(String(25), nullable=True)
open_rate: float = Column(Float) open_rate: float = Column(Float())
open_rate_requested = Column(Float) open_rate_requested = Column(Float())
# open_trade_value - calculated via _calc_open_trade_value # open_trade_value - calculated via _calc_open_trade_value
open_trade_value = Column(Float) open_trade_value = Column(Float())
close_rate: Optional[float] = Column(Float) close_rate: Optional[float] = Column(Float())
close_rate_requested = Column(Float) close_rate_requested = Column(Float())
realized_profit = Column(Float, default=0.0) realized_profit = Column(Float(), default=0.0)
close_profit = Column(Float) close_profit = Column(Float())
close_profit_abs = Column(Float) close_profit_abs = Column(Float())
stake_amount = Column(Float, nullable=False) stake_amount = Column(Float(), nullable=False)
max_stake_amount = Column(Float) max_stake_amount = Column(Float())
amount = Column(Float) amount = Column(Float())
amount_requested = Column(Float) amount_requested = Column(Float())
open_date = Column(DateTime, nullable=False, default=datetime.utcnow) open_date = Column(DateTime(), nullable=False, default=datetime.utcnow)
close_date = Column(DateTime) close_date = Column(DateTime())
open_order_id = Column(String(255)) open_order_id = Column(String(255))
# absolute value of the stop loss # absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0) stop_loss = Column(Float(), nullable=True, default=0.0)
# percentage value of the stop loss # percentage value of the stop loss
stop_loss_pct = Column(Float, nullable=True) stop_loss_pct = Column(Float(), nullable=True)
# absolute value of the initial stop loss # absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=True, default=0.0) initial_stop_loss = Column(Float(), nullable=True, default=0.0)
# percentage value of the initial stop loss # percentage value of the initial stop loss
initial_stop_loss_pct = Column(Float, nullable=True) initial_stop_loss_pct = Column(Float(), nullable=True)
# stoploss order id which is on exchange # stoploss order id which is on exchange
stoploss_order_id = Column(String(255), nullable=True, index=True) stoploss_order_id = Column(String(255), nullable=True, index=True)
# last update time of the stoploss order on exchange # last update time of the stoploss order on exchange
stoploss_last_update = Column(DateTime, nullable=True) stoploss_last_update = Column(DateTime(), nullable=True)
# absolute value of the highest reached price # absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0) max_rate = Column(Float(), nullable=True, default=0.0)
# Lowest price reached # Lowest price reached
min_rate = Column(Float, nullable=True) min_rate = Column(Float(), nullable=True)
exit_reason = Column(String(100), nullable=True) exit_reason = Column(String(100), nullable=True)
exit_order_status = Column(String(100), nullable=True) exit_order_status = Column(String(100), nullable=True)
strategy = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True)
@ -1222,21 +1225,21 @@ class Trade(_DECL_BASE, LocalTrade):
timeframe = Column(Integer, nullable=True) timeframe = Column(Integer, nullable=True)
trading_mode = Column(Enum(TradingMode), nullable=True) trading_mode = Column(Enum(TradingMode), nullable=True)
amount_precision = Column(Float, nullable=True) amount_precision = Column(Float(), nullable=True)
price_precision = Column(Float, nullable=True) price_precision = Column(Float(), nullable=True)
precision_mode = Column(Integer, nullable=True) precision_mode = Column(Integer, nullable=True)
contract_size = Column(Float, nullable=True) contract_size = Column(Float(), nullable=True)
# Leverage trading properties # Leverage trading properties
leverage = Column(Float, nullable=True, default=1.0) leverage = Column(Float(), nullable=True, default=1.0)
is_short = Column(Boolean, nullable=False, default=False) is_short = Column(Boolean, nullable=False, default=False)
liquidation_price = Column(Float, nullable=True) liquidation_price = Column(Float(), nullable=True)
# Margin Trading Properties # Margin Trading Properties
interest_rate = Column(Float, nullable=False, default=0.0) interest_rate = Column(Float(), nullable=False, default=0.0)
# Futures properties # Futures properties
funding_fees = Column(Float, nullable=True, default=None) funding_fees = Column(Float(), nullable=True, default=None)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)

View File

@ -1,3 +1,2 @@
# flake8: noqa: F401 from .rpc import RPC, RPCException, RPCHandler # noqa: F401
from .rpc import RPC, RPCException, RPCHandler from .rpc_manager import RPCManager # noqa: F401
from .rpc_manager import RPCManager

View File

@ -1,2 +1 @@
# flake8: noqa: F401 from .webserver import ApiServer # noqa: F401
from .webserver import ApiServer

View File

@ -10,7 +10,7 @@ from fastapi.exceptions import HTTPException
from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.config_validation import validate_config_consistency
from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result
from freqtrade.enums import BacktestState from freqtrade.enums import BacktestState
from freqtrade.exceptions import DependencyException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.misc import deep_merge_dicts from freqtrade.misc import deep_merge_dicts
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest, from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest,
BacktestResponse) BacktestResponse)
@ -26,9 +26,10 @@ router = APIRouter()
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
# flake8: noqa: C901 async def api_start_backtest( # noqa: C901
async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks, bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
ApiServer._bt['bt_error'] = None
"""Start backtesting if not done so already""" """Start backtesting if not done so already"""
if ApiServer._bgtask_running: if ApiServer._bgtask_running:
raise RPCException('Bot Background task already running') raise RPCException('Bot Background task already running')
@ -60,30 +61,31 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
asyncio.set_event_loop(asyncio.new_event_loop()) asyncio.set_event_loop(asyncio.new_event_loop())
try: try:
# Reload strategy # Reload strategy
lastconfig = ApiServer._bt_last_config lastconfig = ApiServer._bt['last_config']
strat = StrategyResolver.load_strategy(btconfig) strat = StrategyResolver.load_strategy(btconfig)
validate_config_consistency(btconfig) validate_config_consistency(btconfig)
if ( if (
not ApiServer._bt not ApiServer._bt['bt']
or lastconfig.get('timeframe') != strat.timeframe or lastconfig.get('timeframe') != strat.timeframe
or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail') or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail')
or lastconfig.get('timerange') != btconfig['timerange'] or lastconfig.get('timerange') != btconfig['timerange']
): ):
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
ApiServer._bt = Backtesting(btconfig) ApiServer._bt['bt'] = Backtesting(btconfig)
ApiServer._bt.load_bt_data_detail() ApiServer._bt['bt'].load_bt_data_detail()
else: else:
ApiServer._bt.config = btconfig ApiServer._bt['bt'].config = btconfig
ApiServer._bt.init_backtest() ApiServer._bt['bt'].init_backtest()
# Only reload data if timeframe changed. # Only reload data if timeframe changed.
if ( if (
not ApiServer._bt_data not ApiServer._bt['data']
or not ApiServer._bt_timerange or not ApiServer._bt['timerange']
or lastconfig.get('timeframe') != strat.timeframe or lastconfig.get('timeframe') != strat.timeframe
or lastconfig.get('timerange') != btconfig['timerange'] or lastconfig.get('timerange') != btconfig['timerange']
): ):
ApiServer._bt_data, ApiServer._bt_timerange = ApiServer._bt.load_bt_data() ApiServer._bt['data'], ApiServer._bt['timerange'] = ApiServer._bt[
'bt'].load_bt_data()
lastconfig['timerange'] = btconfig['timerange'] lastconfig['timerange'] = btconfig['timerange']
lastconfig['timeframe'] = strat.timeframe lastconfig['timeframe'] = strat.timeframe
@ -91,34 +93,35 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
lastconfig['enable_protections'] = btconfig.get('enable_protections') lastconfig['enable_protections'] = btconfig.get('enable_protections')
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet') lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
ApiServer._bt.enable_protections = btconfig.get('enable_protections', False) ApiServer._bt['bt'].enable_protections = btconfig.get('enable_protections', False)
ApiServer._bt.strategylist = [strat] ApiServer._bt['bt'].strategylist = [strat]
ApiServer._bt.results = {} ApiServer._bt['bt'].results = {}
ApiServer._bt.load_prior_backtest() ApiServer._bt['bt'].load_prior_backtest()
ApiServer._bt.abort = False ApiServer._bt['bt'].abort = False
if (ApiServer._bt.results and if (ApiServer._bt['bt'].results and
strat.get_strategy_name() in ApiServer._bt.results['strategy']): strat.get_strategy_name() in ApiServer._bt['bt'].results['strategy']):
# When previous result hash matches - reuse that result and skip backtesting. # When previous result hash matches - reuse that result and skip backtesting.
logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}') logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}')
else: else:
min_date, max_date = ApiServer._bt.backtest_one_strategy( min_date, max_date = ApiServer._bt['bt'].backtest_one_strategy(
strat, ApiServer._bt_data, ApiServer._bt_timerange) strat, ApiServer._bt['data'], ApiServer._bt['timerange'])
ApiServer._bt.results = generate_backtest_stats( ApiServer._bt['bt'].results = generate_backtest_stats(
ApiServer._bt_data, ApiServer._bt.all_results, ApiServer._bt['data'], ApiServer._bt['bt'].all_results,
min_date=min_date, max_date=max_date) min_date=min_date, max_date=max_date)
if btconfig.get('export', 'none') == 'trades': if btconfig.get('export', 'none') == 'trades':
store_backtest_stats( store_backtest_stats(
btconfig['exportfilename'], ApiServer._bt.results, btconfig['exportfilename'], ApiServer._bt['bt'].results,
datetime.now().strftime("%Y-%m-%d_%H-%M-%S") datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
) )
logger.info("Backtest finished.") logger.info("Backtest finished.")
except DependencyException as e: except (Exception, OperationalException, DependencyException) as e:
logger.info(f"Backtesting caused an error: {e}") logger.exception(f"Backtesting caused an error: {e}")
ApiServer._bt['bt_error'] = str(e)
pass pass
finally: finally:
ApiServer._bgtask_running = False ApiServer._bgtask_running = False
@ -146,13 +149,14 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
return { return {
"status": "running", "status": "running",
"running": True, "running": True,
"step": ApiServer._bt.progress.action if ApiServer._bt else str(BacktestState.STARTUP), "step": (ApiServer._bt['bt'].progress.action if ApiServer._bt['bt']
"progress": ApiServer._bt.progress.progress if ApiServer._bt else 0, else str(BacktestState.STARTUP)),
"progress": ApiServer._bt['bt'].progress.progress if ApiServer._bt['bt'] else 0,
"trade_count": len(LocalTrade.trades), "trade_count": len(LocalTrade.trades),
"status_msg": "Backtest running", "status_msg": "Backtest running",
} }
if not ApiServer._bt: if not ApiServer._bt['bt']:
return { return {
"status": "not_started", "status": "not_started",
"running": False, "running": False,
@ -160,6 +164,14 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
"progress": 0, "progress": 0,
"status_msg": "Backtest not yet executed" "status_msg": "Backtest not yet executed"
} }
if ApiServer._bt['bt_error']:
return {
"status": "error",
"running": False,
"step": "",
"progress": 0,
"status_msg": f"Backtest failed with {ApiServer._bt['bt_error']}"
}
return { return {
"status": "ended", "status": "ended",
@ -167,7 +179,7 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
"status_msg": "Backtest ended", "status_msg": "Backtest ended",
"step": "finished", "step": "finished",
"progress": 1, "progress": 1,
"backtest_result": ApiServer._bt.results, "backtest_result": ApiServer._bt['bt'].results,
} }
@ -182,12 +194,12 @@ def api_delete_backtest(ws_mode=Depends(is_webserver_mode)):
"progress": 0, "progress": 0,
"status_msg": "Backtest running", "status_msg": "Backtest running",
} }
if ApiServer._bt: if ApiServer._bt['bt']:
ApiServer._bt.cleanup() ApiServer._bt['bt'].cleanup()
del ApiServer._bt del ApiServer._bt['bt']
ApiServer._bt = None ApiServer._bt['bt'] = None
del ApiServer._bt_data del ApiServer._bt['data']
ApiServer._bt_data = None ApiServer._bt['data'] = None
logger.info("Backtesting reset") logger.info("Backtesting reset")
return { return {
"status": "reset", "status": "reset",
@ -208,7 +220,7 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
"progress": 0, "progress": 0,
"status_msg": "Backtest ended", "status_msg": "Backtest ended",
} }
ApiServer._bt.abort = True ApiServer._bt['bt'].abort = True
return { return {
"status": "stopping", "status": "stopping",
"running": False, "running": False,
@ -218,14 +230,17 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
} }
@router.get('/backtest/history', response_model=List[BacktestHistoryEntry], tags=['webserver', 'backtest']) @router.get('/backtest/history', response_model=List[BacktestHistoryEntry],
tags=['webserver', 'backtest'])
def api_backtest_history(config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): def api_backtest_history(config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
# Get backtest result history, read from metadata files # Get backtest result history, read from metadata files
return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results') return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')
@router.get('/backtest/history/result', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.get('/backtest/history/result', response_model=BacktestResponse,
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): tags=['webserver', 'backtest'])
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config),
ws_mode=Depends(is_webserver_mode)):
# Get backtest result history, read from metadata files # Get backtest result history, read from metadata files
fn = config['user_data_dir'] / 'backtest_results' / filename fn = config['user_data_dir'] / 'backtest_results' / filename
results: Dict[str, Any] = { results: Dict[str, Any] = {

View File

@ -168,6 +168,7 @@ class ShowConfig(BaseModel):
max_open_trades: IntOrInf max_open_trades: IntOrInf
minimal_roi: Dict[str, Any] minimal_roi: Dict[str, Any]
stoploss: Optional[float] stoploss: Optional[float]
stoploss_on_exchange: bool
trailing_stop: Optional[bool] trailing_stop: Optional[bool]
trailing_stop_positive: Optional[float] trailing_stop_positive: Optional[float]
trailing_stop_positive_offset: Optional[float] trailing_stop_positive_offset: Optional[float]

View File

@ -41,7 +41,8 @@ logger = logging.getLogger(__name__)
# 2.21: Add new_candle messagetype # 2.21: Add new_candle messagetype
# 2.22: Add FreqAI to backtesting # 2.22: Add FreqAI to backtesting
# 2.23: Allow plot config request in webserver mode # 2.23: Allow plot config request in webserver mode
API_VERSION = 2.23 # 2.24: Add cancel_open_order endpoint
API_VERSION = 2.24
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() router_public = APIRouter()
@ -123,6 +124,12 @@ def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_delete(tradeid) return rpc._rpc_delete(tradeid)
@router.delete('/trades/{tradeid}/open-order', response_model=OpenTradeSchema, tags=['trading'])
def cancel_open_order(tradeid: int, rpc: RPC = Depends(get_rpc)):
rpc._rpc_cancel_open_order(tradeid)
return rpc._rpc_trade_status([tradeid])[0]
# TODO: Missing response model # TODO: Missing response model
@router.get('/edge', tags=['info']) @router.get('/edge', tags=['info'])
def edge(rpc: RPC = Depends(get_rpc)): def edge(rpc: RPC = Depends(get_rpc)):

View File

@ -90,7 +90,7 @@ async def _process_consumer_request(
elif type == RPCRequestType.ANALYZED_DF: elif type == RPCRequestType.ANALYZED_DF:
# Limit the amount of candles per dataframe to 'limit' or 1500 # Limit the amount of candles per dataframe to 'limit' or 1500
limit = min(data.get('limit', 1500), 1500) if data else None limit = int(min(data.get('limit', 1500), 1500)) if data else None
pair = data.get('pair', None) if data else None pair = data.get('pair', None) if data else None
# For every pair in the generator, send a separate message # For every pair in the generator, send a separate message

View File

@ -36,10 +36,13 @@ class ApiServer(RPCHandler):
_rpc: RPC _rpc: RPC
# Backtesting type: Backtesting # Backtesting type: Backtesting
_bt = None _bt: Dict[str, Any] = {
_bt_data = None 'bt': None,
_bt_timerange = None 'data': None,
_bt_last_config: Config = {} 'timerange': None,
'last_config': {},
'bt_error': None,
}
_has_rpc: bool = False _has_rpc: bool = False
_bgtask_running: bool = False _bgtask_running: bool = False
_config: Config = {} _config: Config = {}

View File

@ -1,7 +1,6 @@
# flake8: noqa: F401
# isort: off # isort: off
from freqtrade.rpc.api_server.ws.types import WebSocketType from freqtrade.rpc.api_server.ws.types import WebSocketType # noqa: F401
from freqtrade.rpc.api_server.ws.proxy import WebSocketProxy from freqtrade.rpc.api_server.ws.proxy import WebSocketProxy # noqa: F401
from freqtrade.rpc.api_server.ws.serializer import HybridJSONWebSocketSerializer from freqtrade.rpc.api_server.ws.serializer import HybridJSONWebSocketSerializer # noqa: F401
from freqtrade.rpc.api_server.ws.channel import WebSocketChannel from freqtrade.rpc.api_server.ws.channel import WebSocketChannel # noqa: F401
from freqtrade.rpc.api_server.ws.message_stream import MessageStream from freqtrade.rpc.api_server.ws.message_stream import MessageStream # noqa: F401

View File

@ -122,6 +122,7 @@ class RPC:
if config['max_open_trades'] != float('inf') else -1), if config['max_open_trades'] != float('inf') else -1),
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {}, 'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
'stoploss': config.get('stoploss'), 'stoploss': config.get('stoploss'),
'stoploss_on_exchange': config.get('stoploss_on_exchange', False),
'trailing_stop': config.get('trailing_stop'), 'trailing_stop': config.get('trailing_stop'),
'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
@ -812,6 +813,29 @@ class RPC:
else: else:
raise RPCException(f'Failed to enter position for {pair}.') raise RPCException(f'Failed to enter position for {pair}.')
def _rpc_cancel_open_order(self, trade_id: int):
if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running')
with self._freqtrade._exit_lock:
# Query for trade
trade = Trade.get_trades(
trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
).first()
if not trade:
logger.warning('cancel_open_order: Invalid trade_id received.')
raise RPCException('Invalid trade_id.')
if not trade.open_order_id:
logger.warning('cancel_open_order: No open order for trade_id.')
raise RPCException('No open order for trade_id.')
try:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
except ExchangeError as e:
logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True)
raise RPCException("Order not found.")
self._freqtrade.handle_cancel_order(order, trade, CANCEL_REASON['USER_CANCEL'])
Trade.commit()
def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]: def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
""" """
Handler for delete <id>. Handler for delete <id>.

View File

@ -174,6 +174,7 @@ class Telegram(RPCHandler):
self._force_enter, order_side=SignalDirection.SHORT)), self._force_enter, order_side=SignalDirection.SHORT)),
CommandHandler('trades', self._trades), CommandHandler('trades', self._trades),
CommandHandler('delete', self._delete_trade), CommandHandler('delete', self._delete_trade),
CommandHandler(['coo', 'cancel_open_order'], self._cancel_open_order),
CommandHandler('performance', self._performance), CommandHandler('performance', self._performance),
CommandHandler(['buys', 'entries'], self._enter_tag_performance), CommandHandler(['buys', 'entries'], self._enter_tag_performance),
CommandHandler(['sells', 'exits'], self._exit_reason_performance), CommandHandler(['sells', 'exits'], self._exit_reason_performance),
@ -1144,10 +1145,25 @@ class Telegram(RPCHandler):
raise RPCException("Trade-id not set.") raise RPCException("Trade-id not set.")
trade_id = int(context.args[0]) trade_id = int(context.args[0])
msg = self._rpc._rpc_delete(trade_id) msg = self._rpc._rpc_delete(trade_id)
self._send_msg(( self._send_msg(
f"`{msg['result_msg']}`\n" f"`{msg['result_msg']}`\n"
'Please make sure to take care of this asset on the exchange manually.' '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:
"""
Handler for /cancel_open_order <id>.
Cancel open order for tradeid
:param bot: telegram bot
:param update: message update
:return: None
"""
if not context.args or len(context.args) == 0:
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.')
@authorized_only @authorized_only
def _performance(self, update: Update, context: CallbackContext) -> None: def _performance(self, update: Update, context: CallbackContext) -> None:
@ -1456,6 +1472,10 @@ class Telegram(RPCHandler):
"*/fx <trade_id>|all:* `Alias to /forceexit`\n" "*/fx <trade_id>|all:* `Alias to /forceexit`\n"
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}" f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n" "*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/cancel_open_order <trade_id>:* `Cancels open orders for trade. "
"Only valid when the trade has open orders.`\n"
"*/coo <trade_id>|all:* `Alias to /cancel_open_order`\n"
"*/whitelist [sorted] [baseonly]:* `Show current whitelist. Optionally in " "*/whitelist [sorted] [baseonly]:* `Show current whitelist. Optionally in "
"order and/or only displaying the base currency of each pairing.`\n" "order and/or only displaying the base currency of each pairing.`\n"
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "

View File

@ -163,7 +163,7 @@ class HyperStrategyMixin:
else: else:
logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}') logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}')
def get_no_optimize_params(self): def get_no_optimize_params(self) -> Dict[str, Dict]:
""" """
Returns list of Parameters that are not part of the current optimize job Returns list of Parameters that are not part of the current optimize job
""" """
@ -173,7 +173,7 @@ class HyperStrategyMixin:
'protection': {}, 'protection': {},
} }
for name, p in self.enumerate_parameters(): for name, p in self.enumerate_parameters():
if not p.optimize or not p.in_space: if p.category and (not p.optimize or not p.in_space):
params[p.category][name] = p.value params[p.category][name] = p.value
return params return params

View File

@ -614,8 +614,8 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return df return df
def feature_engineering_expand_all(self, dataframe: DataFrame, def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
period: int, **kwargs): metadata: Dict, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined This function will automatically expand the defined features on the config defined
@ -634,13 +634,14 @@ class IStrategy(ABC, HyperStrategyMixin):
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features :param dataframe: strategy dataframe which will receive the features
:param period: period of the indicator - usage example: :param period: period of the indicator - usage example:
:param metadata: metadata of current pair
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
""" """
return dataframe return dataframe
def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs): def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined This function will automatically expand the defined features on the config defined
@ -662,13 +663,14 @@ class IStrategy(ABC, HyperStrategyMixin):
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features :param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
dataframe["%-pct-change"] = dataframe["close"].pct_change() dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200) dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
""" """
return dataframe return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs): def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe. This optional function will be called once with the dataframe of the base timeframe.
@ -686,12 +688,13 @@ class IStrategy(ABC, HyperStrategyMixin):
https://www.freqtrade.io/en/latest/freqai-feature-engineering https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the features :param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7 usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
""" """
return dataframe return dataframe
def set_freqai_targets(self, dataframe, **kwargs): def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
Required function to set the targets for the model. Required function to set the targets for the model.
@ -701,7 +704,8 @@ class IStrategy(ABC, HyperStrategyMixin):
https://www.freqtrade.io/en/latest/freqai-feature-engineering https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the targets :param dataframe: strategy dataframe which will receive the targets
:param metadata: metadata of current pair
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"] usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
""" """
return dataframe return dataframe
@ -1079,10 +1083,10 @@ class IStrategy(ABC, HyperStrategyMixin):
trade.adjust_min_max_rates(high or current_rate, low or current_rate) trade.adjust_min_max_rates(high or current_rate, low or current_rate)
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, stoplossflag = self.ft_stoploss_reached(current_rate=current_rate, trade=trade,
current_time=current_time, current_time=current_time,
current_profit=current_profit, current_profit=current_profit,
force_stoploss=force_stoploss, low=low, high=high) force_stoploss=force_stoploss, low=low, high=high)
# Set current rate to high for backtesting exits # Set current rate to high for backtesting exits
current_rate = (low if trade.is_short else high) or rate current_rate = (low if trade.is_short else high) or rate
@ -1149,13 +1153,12 @@ class IStrategy(ABC, HyperStrategyMixin):
return exits return exits
def stop_loss_reached(self, current_rate: float, trade: Trade, def ft_stoploss_adjust(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float, current_time: datetime, current_profit: float,
force_stoploss: float, low: Optional[float] = None, force_stoploss: float, low: Optional[float] = None,
high: Optional[float] = None) -> ExitCheckTuple: high: Optional[float] = None) -> None:
""" """
Based on current profit of the trade and configured (trailing) stoploss, Adjust stop-loss dynamically if configured to do so.
decides to exit or not
:param current_profit: current profit as ratio :param current_profit: current profit as ratio
:param low: Low value of this candle, only set in backtesting :param low: Low value of this candle, only set in backtesting
:param high: High value of this candle, only set in backtesting :param high: High value of this candle, only set in backtesting
@ -1201,6 +1204,20 @@ class IStrategy(ABC, HyperStrategyMixin):
trade.adjust_stop_loss(bound or current_rate, stop_loss_value) trade.adjust_stop_loss(bound or current_rate, stop_loss_value)
def ft_stoploss_reached(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float,
force_stoploss: float, low: Optional[float] = None,
high: Optional[float] = None) -> ExitCheckTuple:
"""
Based on current profit of the trade and configured (trailing) stoploss,
decides to exit or not
:param current_profit: current profit as ratio
:param low: Low value of this candle, only set in backtesting
:param high: High value of this candle, only set in backtesting
"""
self.ft_stoploss_adjust(current_rate, trade, current_time, current_profit,
force_stoploss, low, high)
sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short) sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short)
sl_lower_short = (trade.stop_loss <= (high or current_rate) and trade.is_short) sl_lower_short = (trade.stop_loss <= (high or current_rate) and trade.is_short)
liq_higher_long = (trade.liquidation_price liq_higher_long = (trade.liquidation_price

View File

@ -1,12 +1,13 @@
import logging import logging
from typing import Dict
import numpy as np import numpy as np # noqa
import pandas as pd import pandas as pd # noqa
import talib.abstract as ta import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
from technical import qtpylib from technical import qtpylib
from freqtrade.strategy import IntParameter, IStrategy, merge_informative_pair from freqtrade.strategy import IntParameter, IStrategy, merge_informative_pair # noqa
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -26,7 +27,7 @@ class FreqaiExampleHybridStrategy(IStrategy):
"freqai": { "freqai": {
"enabled": true, "enabled": true,
"purge_old_models": true, "purge_old_models": 2,
"train_period_days": 15, "train_period_days": 15,
"identifier": "uniqe-id", "identifier": "uniqe-id",
"feature_parameters": { "feature_parameters": {
@ -95,7 +96,8 @@ class FreqaiExampleHybridStrategy(IStrategy):
short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True) short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True)
exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
def feature_engineering_expand_all(self, dataframe, period, **kwargs): def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined This function will automatically expand the defined features on the config defined
@ -114,8 +116,9 @@ class FreqaiExampleHybridStrategy(IStrategy):
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features :param dataframe: strategy dataframe which will receive the features
:param period: period of the indicator - usage example: :param period: period of the indicator - usage example:
:param metadata: metadata of current pair
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
""" """
@ -148,7 +151,7 @@ class FreqaiExampleHybridStrategy(IStrategy):
return dataframe return dataframe
def feature_engineering_expand_basic(self, dataframe, **kwargs): def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined This function will automatically expand the defined features on the config defined
@ -170,7 +173,8 @@ class FreqaiExampleHybridStrategy(IStrategy):
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features :param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
dataframe["%-pct-change"] = dataframe["close"].pct_change() dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200) dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
""" """
@ -179,7 +183,7 @@ class FreqaiExampleHybridStrategy(IStrategy):
dataframe["%-raw_price"] = dataframe["close"] dataframe["%-raw_price"] = dataframe["close"]
return dataframe return dataframe
def feature_engineering_standard(self, dataframe, **kwargs): def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe. This optional function will be called once with the dataframe of the base timeframe.
@ -197,14 +201,15 @@ class FreqaiExampleHybridStrategy(IStrategy):
https://www.freqtrade.io/en/latest/freqai-feature-engineering https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the features :param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7 usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
""" """
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
return dataframe return dataframe
def set_freqai_targets(self, dataframe, **kwargs): def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
Required function to set the targets for the model. Required function to set the targets for the model.
@ -214,16 +219,16 @@ class FreqaiExampleHybridStrategy(IStrategy):
https://www.freqtrade.io/en/latest/freqai-feature-engineering https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the targets :param dataframe: strategy dataframe which will receive the targets
:param metadata: metadata of current pair
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"] usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
""" """
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-50) > dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-50) >
dataframe["close"], 'up', 'down') dataframe["close"], 'up', 'down')
return dataframe return dataframe
# flake8: noqa: C901 def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # noqa: C901
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# User creates their own custom strat here. Present example is a supertrend # User creates their own custom strat here. Present example is a supertrend
# based strategy. # based strategy.

View File

@ -1,5 +1,6 @@
import logging import logging
from functools import reduce from functools import reduce
from typing import Dict
import talib.abstract as ta import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
@ -46,7 +47,8 @@ class FreqaiExampleStrategy(IStrategy):
std_dev_multiplier_sell = CategoricalParameter( std_dev_multiplier_sell = CategoricalParameter(
[0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True) [0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True)
def feature_engineering_expand_all(self, dataframe, period, **kwargs): def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined This function will automatically expand the defined features on the config defined
@ -58,6 +60,10 @@ class FreqaiExampleStrategy(IStrategy):
All features must be prepended with `%` to be recognized by FreqAI internals. All features must be prepended with `%` to be recognized by FreqAI internals.
Access metadata such as the current pair/timeframe with:
`metadata["pair"]` `metadata["tf"]`
More details on how these config defined parameters accelerate feature engineering More details on how these config defined parameters accelerate feature engineering
in the documentation at: in the documentation at:
@ -65,8 +71,9 @@ class FreqaiExampleStrategy(IStrategy):
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features :param dataframe: strategy dataframe which will receive the features
:param period: period of the indicator - usage example: :param period: period of the indicator - usage example:
:param metadata: metadata of current pair
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
""" """
@ -99,7 +106,7 @@ class FreqaiExampleStrategy(IStrategy):
return dataframe return dataframe
def feature_engineering_expand_basic(self, dataframe, **kwargs): def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined This function will automatically expand the defined features on the config defined
@ -114,6 +121,10 @@ class FreqaiExampleStrategy(IStrategy):
All features must be prepended with `%` to be recognized by FreqAI internals. All features must be prepended with `%` to be recognized by FreqAI internals.
Access metadata such as the current pair/timeframe with:
`metadata["pair"]` `metadata["tf"]`
More details on how these config defined parameters accelerate feature engineering More details on how these config defined parameters accelerate feature engineering
in the documentation at: in the documentation at:
@ -121,7 +132,8 @@ class FreqaiExampleStrategy(IStrategy):
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features :param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
dataframe["%-pct-change"] = dataframe["close"].pct_change() dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200) dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
""" """
@ -130,7 +142,7 @@ class FreqaiExampleStrategy(IStrategy):
dataframe["%-raw_price"] = dataframe["close"] dataframe["%-raw_price"] = dataframe["close"]
return dataframe return dataframe
def feature_engineering_standard(self, dataframe, **kwargs): def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe. This optional function will be called once with the dataframe of the base timeframe.
@ -144,28 +156,38 @@ class FreqaiExampleStrategy(IStrategy):
All features must be prepended with `%` to be recognized by FreqAI internals. All features must be prepended with `%` to be recognized by FreqAI internals.
Access metadata such as the current pair with:
`metadata["pair"]`
More details about feature engineering available: More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the features :param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7 usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
""" """
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
return dataframe return dataframe
def set_freqai_targets(self, dataframe, **kwargs): def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
""" """
*Only functional with FreqAI enabled strategies* *Only functional with FreqAI enabled strategies*
Required function to set the targets for the model. Required function to set the targets for the model.
All targets must be prepended with `&` to be recognized by the FreqAI internals. All targets must be prepended with `&` to be recognized by the FreqAI internals.
Access metadata such as the current pair with:
`metadata["pair"]`
More details about feature engineering available: More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the targets :param dataframe: strategy dataframe which will receive the targets
:param metadata: metadata of current pair
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"] usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
""" """
dataframe["&-s_close"] = ( dataframe["&-s_close"] = (

View File

@ -118,6 +118,7 @@
"from freqtrade.data.dataprovider import DataProvider\n", "from freqtrade.data.dataprovider import DataProvider\n",
"strategy = StrategyResolver.load_strategy(config)\n", "strategy = StrategyResolver.load_strategy(config)\n",
"strategy.dp = DataProvider(config, None, None)\n", "strategy.dp = DataProvider(config, None, None)\n",
"strategy.ft_bot_start()\n",
"\n", "\n",
"# Generate buy/sell signals using strategy\n", "# Generate buy/sell signals using strategy\n",
"df = strategy.analyze_ticker(candles, {'pair': pair})\n", "df = strategy.analyze_ticker(candles, {'pair': pair})\n",

View File

@ -1,3 +1,2 @@
# flake8: noqa: F401 from freqtrade.util.ft_precise import FtPrecise # noqa: F401
from freqtrade.util.ft_precise import FtPrecise from freqtrade.util.periodic_cache import PeriodicCache # noqa: F401
from freqtrade.util.periodic_cache import PeriodicCache

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# QTPyLib: Quantitative Trading Python Library # QTPyLib: Quantitative Trading Python Library

0
freqtrade/worker.py Executable file → Normal file
View File

View File

@ -9,25 +9,25 @@
coveralls==3.3.1 coveralls==3.3.1
flake8==6.0.0 flake8==6.0.0
flake8-tidy-imports==4.8.0 flake8-tidy-imports==4.8.0
mypy==0.991 mypy==1.0.1
pre-commit==2.21.0 pre-commit==3.0.4
pytest==7.2.1 pytest==7.2.1
pytest-asyncio==0.20.3 pytest-asyncio==0.20.3
pytest-cov==4.0.0 pytest-cov==4.0.0
pytest-mock==3.10.0 pytest-mock==3.10.0
pytest-random-order==1.1.0 pytest-random-order==1.1.0
isort==5.11.4 isort==5.12.0
# For datetime mocking # For datetime mocking
time-machine==2.9.0 time-machine==2.9.0
# fastapi testing # fastapi testing
httpx==0.23.3 httpx==0.23.3
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==7.2.8 nbconvert==7.2.9
# mypy types # mypy types
types-cachetools==5.2.1 types-cachetools==5.3.0.0
types-filelock==3.2.7 types-filelock==3.2.7
types-requests==2.28.11.8 types-requests==2.28.11.13
types-tabulate==0.9.0.0 types-tabulate==0.9.0.0
types-python-dateutil==2.8.19.6 types-python-dateutil==2.8.19.6

View File

@ -3,7 +3,8 @@
# Required for freqai-rl # Required for freqai-rl
torch==1.13.1 torch==1.13.1
stable-baselines3==1.6.2 stable-baselines3==1.7.0
sb3-contrib==1.6.2 sb3-contrib==1.7.0
# Gym is forced to this version by stable-baselines3. # Gym is forced to this version by stable-baselines3.
setuptools==65.5.1 # Should be removed when gym is fixed.
gym==0.21 gym==0.21

View File

@ -6,6 +6,6 @@
scikit-learn==1.1.3 scikit-learn==1.1.3
joblib==1.2.0 joblib==1.2.0
catboost==1.1.1; platform_machine != 'aarch64' catboost==1.1.1; platform_machine != 'aarch64'
lightgbm==3.3.4 lightgbm==3.3.5
xgboost==1.7.3 xgboost==1.7.3
tensorboard==2.11.2 tensorboard==2.12.0

View File

@ -2,7 +2,7 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # Required for hyperopt
scipy==1.10.0 scipy==1.10.1
scikit-learn==1.1.3 scikit-learn==1.1.3
scikit-optimize==0.9.0 scikit-optimize==0.9.0
filelock==3.9.0 filelock==3.9.0

View File

@ -1,4 +1,4 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==5.11.0 plotly==5.13.0

View File

@ -1,12 +1,10 @@
numpy==1.24.1 numpy==1.24.2
pandas==1.5.3 pandas==1.5.3
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==2.7.12 ccxt==2.8.17
# Pin cryptography for now due to rust build errors with piwheels cryptography==39.0.1
cryptography==38.0.1; platform_machine == 'armv7l' aiohttp==3.8.4
cryptography==39.0.0; platform_machine != 'armv7l'
aiohttp==3.8.3
SQLAlchemy==1.4.46 SQLAlchemy==1.4.46
python-telegram-bot==13.15 python-telegram-bot==13.15
arrow==1.2.3 arrow==1.2.3
@ -15,14 +13,14 @@ requests==2.28.2
urllib3==1.26.14 urllib3==1.26.14
jsonschema==4.17.3 jsonschema==4.17.3
TA-Lib==0.4.25 TA-Lib==0.4.25
technical==1.3.0 technical==1.4.0
tabulate==0.9.0 tabulate==0.9.0
pycoingecko==3.1.0 pycoingecko==3.1.0
jinja2==3.1.2 jinja2==3.1.2
tables==3.8.0 tables==3.8.0
blosc==1.11.1 blosc==1.11.1
joblib==1.2.0 joblib==1.2.0
pyarrow==10.0.1; platform_machine != 'armv7l' pyarrow==11.0.0; platform_machine != 'armv7l'
# find first, C search in arrays # find first, C search in arrays
py_find_1st==1.1.5 py_find_1st==1.1.5
@ -30,17 +28,17 @@ py_find_1st==1.1.5
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.9 python-rapidjson==1.9
# Properly format api responses # Properly format api responses
orjson==3.8.5 orjson==3.8.6
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.89.1 fastapi==0.92.0
pydantic==1.10.4 pydantic==1.10.4
uvicorn==0.20.0 uvicorn==0.20.0
pyjwt==2.6.0 pyjwt==2.6.0
aiofiles==22.1.0 aiofiles==23.1.0
psutil==5.9.4 psutil==5.9.4
# Support for colorized terminal output # Support for colorized terminal output

View File

@ -177,8 +177,7 @@ class FtRestClient():
return self._get("version") return self._get("version")
def show_config(self): def show_config(self):
""" """ Returns part of the configuration, relevant for trading operations.
Returns part of the configuration, relevant for trading operations.
:return: json object containing the version :return: json object containing the version
""" """
return self._get("show_config") return self._get("show_config")
@ -232,6 +231,14 @@ class FtRestClient():
""" """
return self._delete(f"trades/{trade_id}") return self._delete(f"trades/{trade_id}")
def cancel_open_order(self, trade_id):
"""Cancel open order for trade.
:param trade_id: Cancels open orders for this trade.
:return: json object
"""
return self._delete(f"trades/{trade_id}/open-order")
def whitelist(self): def whitelist(self):
"""Show the current whitelist. """Show the current whitelist.

0
scripts/ws_client.py Normal file → Executable file
View File

View File

@ -49,48 +49,50 @@ function updateenv() {
source .env/bin/activate source .env/bin/activate
SYS_ARCH=$(uname -m) SYS_ARCH=$(uname -m)
echo "pip install in-progress. Please wait..." echo "pip install in-progress. Please wait..."
${PYTHON} -m pip install --upgrade pip # Setuptools 65.5.0 is the last version that can install gym==0.21.0
read -p "Do you want to install dependencies for dev [y/N]? " ${PYTHON} -m pip install --upgrade pip wheel setuptools==65.5.1
REQUIREMENTS_HYPEROPT=""
REQUIREMENTS_PLOT=""
REQUIREMENTS_FREQAI=""
REQUIREMENTS_FREQAI_RL=""
REQUIREMENTS=requirements.txt
read -p "Do you want to install dependencies for development (Performs a full install with all dependencies) [y/N]? "
dev=$REPLY dev=$REPLY
if [[ $REPLY =~ ^[Yy]$ ]] if [[ $REPLY =~ ^[Yy]$ ]]
then then
REQUIREMENTS=requirements-dev.txt REQUIREMENTS=requirements-dev.txt
else else
REQUIREMENTS=requirements.txt # requirements-dev.txt includes all the below requirements already, so further questions are pointless.
fi read -p "Do you want to install plotting dependencies (plotly) [y/N]? "
REQUIREMENTS_HYPEROPT=""
REQUIREMENTS_PLOT=""
read -p "Do you want to install plotting dependencies (plotly) [y/N]? "
if [[ $REPLY =~ ^[Yy]$ ]]
then
REQUIREMENTS_PLOT="-r requirements-plot.txt"
fi
if [ "${SYS_ARCH}" == "armv7l" ] || [ "${SYS_ARCH}" == "armv6l" ]; then
echo "Detected Raspberry, installing cython, skipping hyperopt installation."
${PYTHON} -m pip install --upgrade cython
else
# Is not Raspberry
read -p "Do you want to install hyperopt dependencies [y/N]? "
if [[ $REPLY =~ ^[Yy]$ ]] if [[ $REPLY =~ ^[Yy]$ ]]
then then
REQUIREMENTS_HYPEROPT="-r requirements-hyperopt.txt" REQUIREMENTS_PLOT="-r requirements-plot.txt"
fi
if [ "${SYS_ARCH}" == "armv7l" ] || [ "${SYS_ARCH}" == "armv6l" ]; then
echo "Detected Raspberry, installing cython, skipping hyperopt installation."
${PYTHON} -m pip install --upgrade cython
else
# Is not Raspberry
read -p "Do you want to install hyperopt dependencies [y/N]? "
if [[ $REPLY =~ ^[Yy]$ ]]
then
REQUIREMENTS_HYPEROPT="-r requirements-hyperopt.txt"
fi
fi fi
fi
REQUIREMENTS_FREQAI="" read -p "Do you want to install dependencies for freqai [y/N]? "
REQUIREMENTS_FREQAI_RL=""
read -p "Do you want to install dependencies for freqai [y/N]? "
dev=$REPLY
if [[ $REPLY =~ ^[Yy]$ ]]
then
REQUIREMENTS_FREQAI="-r requirements-freqai.txt --use-pep517"
read -p "Do you also want dependencies for freqai-rl (~700mb additional space required) [y/N]? "
dev=$REPLY
if [[ $REPLY =~ ^[Yy]$ ]] if [[ $REPLY =~ ^[Yy]$ ]]
then then
REQUIREMENTS_FREQAI="-r requirements-freqai-rl.txt" REQUIREMENTS_FREQAI="-r requirements-freqai.txt --use-pep517"
read -p "Do you also want dependencies for freqai-rl (~700mb additional space required) [y/N]? "
if [[ $REPLY =~ ^[Yy]$ ]]
then
REQUIREMENTS_FREQAI="-r requirements-freqai-rl.txt"
fi
fi fi
fi fi
install_talib
${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} ${REQUIREMENTS_FREQAI} ${REQUIREMENTS_FREQAI_RL} ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} ${REQUIREMENTS_FREQAI} ${REQUIREMENTS_FREQAI_RL}
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@ -168,21 +170,18 @@ function install_macos() {
if [[ $version -ge 9 ]]; then #Checks if python version >= 3.9 if [[ $version -ge 9 ]]; then #Checks if python version >= 3.9
install_mac_newer_python_dependencies install_mac_newer_python_dependencies
fi fi
install_talib
} }
# Install bot Debian_ubuntu # Install bot Debian_ubuntu
function install_debian() { function install_debian() {
sudo apt-get update sudo apt-get update
sudo apt-get install -y gcc build-essential autoconf libtool pkg-config make wget git curl $(echo lib${PYTHON}-dev ${PYTHON}-venv) sudo apt-get install -y gcc build-essential autoconf libtool pkg-config make wget git curl $(echo lib${PYTHON}-dev ${PYTHON}-venv)
install_talib
} }
# Install bot RedHat_CentOS # Install bot RedHat_CentOS
function install_redhat() { function install_redhat() {
sudo yum update sudo yum update
sudo yum install -y gcc gcc-c++ make autoconf libtool pkg-config wget git $(echo ${PYTHON}-devel | sed 's/\.//g') sudo yum install -y gcc gcc-c++ make autoconf libtool pkg-config wget git $(echo ${PYTHON}-devel | sed 's/\.//g')
install_talib
} }
# Upgrade the bot # Upgrade the bot
@ -191,26 +190,37 @@ function update() {
updateenv updateenv
} }
function check_git_changes() {
if [ -z "$(git status --porcelain)" ]; then
echo "No changes in git directory"
return 1
else
echo "Changes in git directory"
return 0
fi
}
# Reset Develop or Stable branch # Reset Develop or Stable branch
function reset() { function reset() {
echo_block "Resetting branch and virtual env" echo_block "Resetting branch and virtual env"
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* stable") ] if [ "1" == $(git branch -vv |grep -cE "\* develop|\* stable") ]
then then
if check_git_changes; then
read -p "Keep your local changes? (Otherwise will remove all changes you made!) [Y/n]? "
if [[ $REPLY =~ ^[Nn]$ ]]; then
read -p "Reset git branch? (This will remove all changes you made!) [y/N]? " git fetch -a
if [[ $REPLY =~ ^[Yy]$ ]]; then
git fetch -a if [ "1" == $(git branch -vv | grep -c "* develop") ]
then
if [ "1" == $(git branch -vv | grep -c "* develop") ] echo "- Hard resetting of 'develop' branch."
then git reset --hard origin/develop
echo "- Hard resetting of 'develop' branch." elif [ "1" == $(git branch -vv | grep -c "* stable") ]
git reset --hard origin/develop then
elif [ "1" == $(git branch -vv | grep -c "* stable") ] echo "- Hard resetting of 'stable' branch."
then git reset --hard origin/stable
echo "- Hard resetting of 'stable' branch." fi
git reset --hard origin/stable
fi fi
fi fi
else else

View File

@ -2573,7 +2573,7 @@ def import_fails() -> None:
realimport = builtins.__import__ realimport = builtins.__import__
def mockedimport(name, *args, **kwargs): def mockedimport(name, *args, **kwargs):
if name in ["filelock", 'systemd.journal', 'uvloop']: if name in ["filelock", 'cysystemd.journal', 'uvloop']:
raise ImportError(f"No module named '{name}'") raise ImportError(f"No module named '{name}'")
return realimport(name, *args, **kwargs) return realimport(name, *args, **kwargs)

Some files were not shown because too many files have changed in this diff Show More