Compare commits

..

32 Commits

Author SHA1 Message Date
Matthias
046ae18411 Merge pull request #7144 from freqtrade/new_release
New release 2022.7
2022-07-30 16:06:37 +02:00
Matthias
28b4773083 Version bump 2022.7 2022-07-30 09:21:29 +02:00
Matthias
d4e8ab1cac Merge branch 'stable' into new_release 2022-07-30 09:21:05 +02:00
Matthias
2db5cc177d Merge pull request #7029 from freqtrade/new_release
New release 2022.6
2022-07-03 19:42:24 +02:00
Matthias
c1d4078518 Version bump to 2022.6 2022-07-03 15:04:38 +02:00
Matthias
d25ec6d0b8 Merge branch 'stable' into new_release 2022-07-03 15:04:16 +02:00
Matthias
c57db0a330 Version bump 2022.5.1 2022-06-01 06:34:28 +02:00
Matthias
f5087a82dc Merge branch 'stable' into new_release 2022-06-01 06:33:42 +02:00
Matthias
eed0d67005 Merge pull request #6893 from freqtrade/new_release
New release 2022.5
2022-05-28 13:46:24 +02:00
Matthias
a1d54f5ae0 Version bump 2022.5 2022-05-28 09:49:58 +02:00
Matthias
a4a7c6536d Merge branch 'stable' into new_release 2022-05-28 09:49:46 +02:00
Matthias
340a97d1df Merge pull request #6811 from DJCrashdummy/patch-1
corrected minor "typo" in formatting
2022-05-10 19:16:40 +02:00
DJCrashdummy
fab197edf2 corrected minor "typo" in formatting 2022-05-10 10:33:04 +00:00
Matthias
851c5dad30 Version bump 2022.4.2 2022-05-03 20:37:29 +02:00
Matthias
5b76ae452f Fix fee handling for futures trades 2022-05-03 20:35:30 +02:00
Matthias
2c750fdb09 Reduce no stake amount verbosity
closes #6768
2022-05-03 20:35:22 +02:00
Matthias
e7f5252074 Version bump 2022.4.1 2022-05-01 16:49:11 +02:00
Matthias
dfbd1c34c4 Merge pull request #6755 from freqtrade/new_release
New release 2022.4
2022-05-01 14:51:39 +02:00
Matthias
7615c4e904 Version bump 2022.4 2022-05-01 11:19:32 +02:00
Matthias
e9b78bf3ae Merge branch 'stable' into new_release 2022-05-01 11:19:17 +02:00
Matthias
2e397a88e1 Merge pull request #6592 from freqtrade/new_release
New release 2022.3
2022-03-27 15:51:58 +02:00
Matthias
fe6c62e144 Version bump 2022.3 2022-03-27 15:27:16 +02:00
Matthias
f0db721f05 Merge branch 'stable' into new_release 2022-03-27 15:09:06 +02:00
Matthias
4d8d30ea39 Version bump to 2022.2.2 2022-03-21 06:34:33 +01:00
Matthias
e90e3cead0 Map usdt fiat to correct coingecko fiat 2022-03-21 06:34:20 +01:00
Matthias
a568548192 Merge pull request #6464 from freqtrade/new_release
New release 2022.2.1
2022-02-26 08:57:42 +01:00
Matthias
f9d10a7fad Version bump 2022.2.1 2022-02-26 08:35:50 +01:00
Matthias
cbc2b00ee6 Merge branch 'stable' into new_release 2022-02-26 08:35:31 +01:00
Matthias
8f7b857ae9 Merge pull request #6459 from freqtrade/new_release
New release 2022.2
2022-02-25 15:14:27 +01:00
Matthias
e88b022cd4 Version bump 2022.2 2022-02-25 12:07:09 +01:00
Matthias
bfb738f69f Merge branch 'stable' into new_release 2022-02-25 12:06:11 +01:00
Matthias
00dd8e76ee Merge pull request #6416 from froggleston/patch-2
Update windows_installation.md
2022-02-25 11:44:40 +01:00
137 changed files with 1167 additions and 8695 deletions

View File

@@ -351,7 +351,7 @@ jobs:
python setup.py sdist bdist_wheel python setup.py sdist bdist_wheel
- name: Publish to PyPI (Test) - name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.5.1 uses: pypa/gh-action-pypi-publish@v1.5.0
if: (github.event_name == 'release') if: (github.event_name == 'release')
with: with:
user: __token__ user: __token__
@@ -359,7 +359,7 @@ jobs:
repository_url: https://test.pypi.org/legacy/ repository_url: https://test.pypi.org/legacy/
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.5.1 uses: pypa/gh-action-pypi-publish@v1.5.0
if: (github.event_name == 'release') if: (github.event_name == 'release')
with: with:
user: __token__ user: __token__

6
.gitignore vendored
View File

@@ -7,15 +7,10 @@ logfile.txt
user_data/* user_data/*
!user_data/strategy/sample_strategy.py !user_data/strategy/sample_strategy.py
!user_data/notebooks !user_data/notebooks
!user_data/models
!user_data/freqaimodels
user_data/freqaimodels/*
user_data/models/*
user_data/notebooks/* user_data/notebooks/*
freqtrade-plot.html freqtrade-plot.html
freqtrade-profit-plot.html freqtrade-profit-plot.html
freqtrade/rpc/api_server/ui/* freqtrade/rpc/api_server/ui/*
build_helpers/ta-lib/*
# Macos related # Macos related
.DS_Store .DS_Store
@@ -112,4 +107,3 @@ target/
!config_examples/config_ftx.example.json !config_examples/config_ftx.example.json
!config_examples/config_full.example.json !config_examples/config_full.example.json
!config_examples/config_kraken.example.json !config_examples/config_kraken.example.json
!config_examples/config_freqai.example.json

View File

@@ -15,7 +15,7 @@ repos:
additional_dependencies: additional_dependencies:
- types-cachetools==5.2.1 - types-cachetools==5.2.1
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.28.8 - types-requests==2.28.3
- types-tabulate==0.8.11 - types-tabulate==0.8.11
- types-python-dateutil==2.8.19 - types-python-dateutil==2.8.19
# stages: [push] # stages: [push]

View File

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

View File

@@ -63,7 +63,6 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
- [x] **Dry-run**: Run the bot without paying money. - [x] **Dry-run**: Run the bot without paying money.
- [x] **Backtesting**: Run a simulation of your buy/sell strategy. - [x] **Backtesting**: Run a simulation of your buy/sell strategy.
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. - [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
- [X] **Adaptive prediction modeling**: Build a smart strategy with FreqAI that self-trains to the market via adaptive machine learning methods. [Learn more](https://www.freqtrade.io/en/stable/freqai/)
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/). - [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/).
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists. - [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid. - [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
@@ -194,7 +193,7 @@ Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/
The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges. The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges.
### Minimum hardware required ### Min hardware required
To run this bot we recommend you a cloud instance with a minimum of: To run this bot we recommend you a cloud instance with a minimum of:

View File

@@ -4,7 +4,7 @@ else
INSTALL_LOC=${1} INSTALL_LOC=${1}
fi fi
echo "Installing to ${INSTALL_LOC}" echo "Installing to ${INSTALL_LOC}"
if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
tar zxvf ta-lib-0.4.0-src.tar.gz tar zxvf ta-lib-0.4.0-src.tar.gz
cd ta-lib \ cd ta-lib \
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
@@ -17,17 +17,11 @@ if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
cd .. && rm -rf ./ta-lib/ cd .. && rm -rf ./ta-lib/
exit 1 exit 1
fi fi
if [ -z "$2" ]; then
which sudo && sudo make install || make install which sudo && sudo make install || make install
if [ -x "$(command -v apt-get)" ]; then if [ -x "$(command -v apt-get)" ]; then
echo "Updating library path using ldconfig" echo "Updating library path using ldconfig"
sudo ldconfig sudo ldconfig
fi fi
else
# Don't install with sudo
make install
fi
cd .. && rm -rf ./ta-lib/ cd .. && rm -rf ./ta-lib/
else else
echo "TA-lib already installed, skipping installation" echo "TA-lib already installed, skipping installation"

View File

@@ -6,12 +6,10 @@ export DOCKER_BUILDKIT=1
# Replace / with _ to create a valid tag # Replace / with _ to create a valid tag
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
TAG_PLOT=${TAG}_plot TAG_PLOT=${TAG}_plot
TAG_FREQAI=${TAG}_freqai
TAG_PI="${TAG}_pi" TAG_PI="${TAG}_pi"
TAG_ARM=${TAG}_arm TAG_ARM=${TAG}_arm
TAG_PLOT_ARM=${TAG_PLOT}_arm TAG_PLOT_ARM=${TAG_PLOT}_arm
TAG_FREQAI_ARM=${TAG_FREQAI}_arm
CACHE_IMAGE=freqtradeorg/freqtrade_cache CACHE_IMAGE=freqtradeorg/freqtrade_cache
echo "Running for ${TAG}" echo "Running for ${TAG}"
@@ -40,10 +38,8 @@ fi
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot . docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM
# Run backtest # Run backtest
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
@@ -57,7 +53,6 @@ docker images
# docker push ${IMAGE_NAME} # docker push ${IMAGE_NAME}
docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM
docker push ${CACHE_IMAGE}:$TAG_FREQAI_ARM
docker push ${CACHE_IMAGE}:$TAG_ARM docker push ${CACHE_IMAGE}:$TAG_ARM
# Create multi-arch image # Create multi-arch image
@@ -71,9 +66,6 @@ docker manifest push -p ${IMAGE_NAME}:${TAG}
docker manifest create ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} ${CACHE_IMAGE}:${TAG_PLOT} docker manifest create ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} ${CACHE_IMAGE}:${TAG_PLOT}
docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT} docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT}
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM} ${CACHE_IMAGE}:${TAG_FREQAI}
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI}
# Tag as latest for develop builds # Tag as latest for develop builds
if [ "${TAG}" = "develop" ]; then if [ "${TAG}" = "develop" ]; then
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG} docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}

View File

@@ -5,7 +5,6 @@
# Replace / with _ to create a valid tag # Replace / with _ to create a valid tag
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
TAG_PLOT=${TAG}_plot TAG_PLOT=${TAG}_plot
TAG_FREQAI=${TAG}_freqai
TAG_PI="${TAG}_pi" TAG_PI="${TAG}_pi"
PI_PLATFORM="linux/arm/v7" PI_PLATFORM="linux/arm/v7"
@@ -50,10 +49,8 @@ fi
docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI
# Run backtest # Run backtest
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
@@ -67,7 +64,6 @@ docker images
docker push ${CACHE_IMAGE} docker push ${CACHE_IMAGE}
docker push ${CACHE_IMAGE}:$TAG_PLOT docker push ${CACHE_IMAGE}:$TAG_PLOT
docker push ${CACHE_IMAGE}:$TAG_FREQAI
docker push ${CACHE_IMAGE}:$TAG docker push ${CACHE_IMAGE}:$TAG

View File

@@ -1,96 +0,0 @@
{
"trading_mode": "futures",
"margin_mode": "isolated",
"max_open_trades": 5,
"stake_currency": "USDT",
"stake_amount": 200,
"tradable_balance_ratio": 1,
"fiat_display_currency": "USD",
"dry_run": true,
"timeframe": "3m",
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": true,
"unfilledtimeout": {
"entry": 10,
"exit": 30
},
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {
"enableRateLimit": true
},
"ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 200
},
"pair_whitelist": [
"1INCH/USDT",
"ALGO/USDT"
],
"pair_blacklist": []
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "other",
"use_order_book": true,
"order_book_top": 1
},
"pairlists": [
{
"method": "StaticPairList"
}
],
"freqai": {
"enabled": true,
"startup_candles": 10000,
"purge_old_models": true,
"train_period_days": 15,
"backtest_period_days": 7,
"live_retrain_hours": 0,
"identifier": "uniqe-id",
"feature_parameters": {
"include_timeframes": [
"3m",
"15m",
"1h"
],
"include_corr_pairlist": [
"BTC/USDT",
"ETH/USDT"
],
"label_period_candles": 20,
"include_shifted_candles": 2,
"DI_threshold": 0.9,
"weight_factor": 0.9,
"principal_component_analysis": false,
"use_SVM_to_remove_outliers": true,
"stratify_training_data": 0,
"indicator_max_period_candles": 20,
"indicator_periods_candles": [10, 20]
},
"data_split_parameters": {
"test_size": 0.33,
"random_state": 1
},
"model_training_parameters": {
"n_estimators": 1000
}
},
"bot_name": "",
"force_entry_enable": true,
"initial_state": "running",
"internals": {
"process_throttle_secs": 5
}
}

View File

@@ -5,7 +5,6 @@
"tradable_balance_ratio": 0.99, "tradable_balance_ratio": 0.99,
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"amount_reserve_percent": 0.05, "amount_reserve_percent": 0.05,
"available_capital": 1000,
"amend_last_stake_amount": false, "amend_last_stake_amount": false,
"last_stake_amount_min_ratio": 0.5, "last_stake_amount_min_ratio": 0.5,
"dry_run": true, "dry_run": true,
@@ -93,7 +92,6 @@
"secret": "your_exchange_secret", "secret": "your_exchange_secret",
"password": "", "password": "",
"log_responses": false, "log_responses": false,
// "unknown_fee_rate": 1,
"ccxt_config": {}, "ccxt_config": {},
"ccxt_async_config": {}, "ccxt_async_config": {},
"pair_whitelist": [ "pair_whitelist": [

View File

@@ -1,9 +0,0 @@
ARG sourceimage=freqtradeorg/freqtrade
ARG sourcetag=develop
FROM ${sourceimage}:${sourcetag}
# Install dependencies
COPY requirements-freqai.txt /freqtrade/
RUN pip install -r requirements-freqai.txt --user --no-cache-dir

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

View File

@@ -514,7 +514,6 @@ You can then load the trades to perform further analysis as shown in the [data a
Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions:
- Exchange [trading limits](#trading-limits-in-backtesting) are respected
- Buys happen at open-price - Buys happen at open-price
- All orders are filled at the requested price (no slippage, no unfilled orders) - All orders are filled at the requested price (no slippage, no unfilled orders)
- Exit-signal exits happen at open-price of the consecutive candle - Exit-signal exits happen at open-price of the consecutive candle
@@ -544,24 +543,7 @@ Also, keep in mind that past results don't guarantee future success.
In addition to the above assumptions, strategy authors should carefully read the [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies) section, to avoid using data in backtesting which is not available in real market conditions. In addition to the above assumptions, strategy authors should carefully read the [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies) section, to avoid using data in backtesting which is not available in real market conditions.
### Trading limits in backtesting ### Improved backtest accuracy
Exchanges have certain trading limits, like minimum base currency, or minimum stake (quote) currency.
These limits are usually listed in the exchange documentation as "trading rules" or similar.
Backtesting (as well as live and dry-run) does honor these limits, and will ensure that a stoploss can be placed below this value - so the value will be slightly higher than what the exchange specifies.
Freqtrade has however no information about historic limits.
This can lead to situations where trading-limits are inflated by using a historic price, resulting in minimum amounts > 50$.
For example:
BTC minimum tradable amount is 0.001.
BTC trades at 22.000\$ today (0.001 BTC is related to this) - but the backtesting period includes prices as high as 50.000\$.
Today's minimum would be `0.001 * 22_000` - or 22\$.
However the limit could also be 50$ - based on `0.001 * 50_000` in some historic setting.
## Improved backtest accuracy
One big limitation of backtesting is it's inability to know how prices moved intra-candle (was high before close, or viceversa?). One big limitation of backtesting is it's inability to know how prices moved intra-candle (was high before close, or viceversa?).
So assuming you run backtesting with a 1h timeframe, there will be 4 prices for that candle (Open, High, Low, Close). So assuming you run backtesting with a 1h timeframe, there will be 4 prices for that candle (Open, High, Low, Close).

View File

@@ -105,7 +105,7 @@ This is similar to using multiple `--config` parameters, but simpler in usage as
``` json title="Result" ``` json title="Result"
{ {
"max_open_trades": 3, "max_open_trades": 10,
"stake_currency": "USDT", "stake_currency": "USDT",
"stake_amount": "unlimited" "stake_amount": "unlimited"
} }

View File

@@ -68,36 +68,6 @@ def test_method_to_test(caplog):
``` ```
### Debug configuration
To debug freqtrade, we recommend VSCode with the following launch configuration (located in `.vscode/launch.json`).
Details will obviously vary between setups - but this should work to get you started.
``` json
{
"name": "freqtrade trade",
"type": "python",
"request": "launch",
"module": "freqtrade",
"console": "integratedTerminal",
"args": [
"trade",
// Optional:
// "--userdir", "user_data",
"--strategy",
"MyAwesomeStrategy",
]
},
```
Command line arguments can be added in the `"args"` array.
This method can also be used to debug a strategy, by setting the breakpoints within the strategy.
A similar setup can also be taken for Pycharm - using `freqtrade` as module name, and setting the command line arguments as "parameters".
!!! Note "Startup directory"
This assumes that you have the repository checked out, and the editor is started at the repository root level (so setup.py is at the top level of your repository).
## ErrorHandling ## ErrorHandling
Freqtrade Exceptions all inherit from `FreqtradeException`. Freqtrade Exceptions all inherit from `FreqtradeException`.

View File

@@ -1,769 +0,0 @@
![freqai-logo](assets/freqai_doc_logo.svg)
# FreqAI
FreqAI is a module designed to automate a variety of tasks associated with training a predictive model to generate market forecasts given a set of input features.
Among the the features included:
* **Self-adaptive retraining**: retrain models during live deployments to self-adapt to the market in an unsupervised manner.
* **Rapid feature engineering**: create large rich feature sets (10k+ features) based on simple user created strategies.
* **High performance**: adaptive retraining occurs on separate thread (or on GPU if available) from inferencing and bot trade operations. Keep newest models and data in memory for rapid inferencing.
* **Realistic backtesting**: emulate self-adaptive retraining with backtesting module that automates past retraining.
* **Modifiable**: use the generalized and robust architecture for incorporating any machine learning library/method available in Python. Seven examples available.
* **Smart outlier removal**: remove outliers from training and prediction sets using a variety of outlier detection techniques.
* **Crash resilience**: model storage to disk to make reloading from a crash fast and easy (and purge obsolete files for sustained dry/live runs).
* **Automated data normalization**: normalize the data in a smart and statistically safe way.
* **Automatic data download**: compute the data download timerange and update historic data (in live deployments).
* **Clean incoming data** safe NaN handling before training and prediction.
* **Dimensionality reduction**: reduce the size of the training data via Principal Component Analysis.
* **Deploy bot fleets**: set one bot to train models while a fleet of other bots inference into the models and handle trades.
## Quick start
The easiest way to quickly test FreqAI is to run it in dry run with the following command
```bash
freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates
```
where the user will see the boot-up process of auto-data downloading, followed by simultaneous training and trading.
The example strategy, example prediction model, and example config can all be found in
`freqtrade/templates/FreqaiExampleStrategy.py`, `freqtrade/freqai/prediction_models/LightGBMRegressor.py`,
`config_examples/config_freqai.example.json`, respectively.
## General approach
The user provides FreqAI with a set of custom *base* indicators (created inside the strategy the same way
a typical Freqtrade strategy is created) as well as target values which look into the future.
FreqAI trains a model to predict the target value based on the input of custom indicators for each pair in the whitelist. These models are consistently retrained to adapt to market conditions. FreqAI offers the ability to both backtest strategies (emulating reality with periodic retraining) and deploy dry/live. In dry/live conditions, FreqAI can be set to constant retraining in a background thread in an effort to keep models as young as possible.
An overview of the algorithm is shown here to help users understand the data processing pipeline and the model usage.
![freqai-algo](assets/freqai_algo.png)
## Background and vocabulary
**Features** are the quantities with which a model is trained. $X_i$ represents the
vector of all features for a single candle. In FreqAI, the user
builds the features from anything they can construct in the strategy.
**Labels** are the target values with which the weights inside a model are trained
toward. Each set of features is associated with a single label, which is also
defined within the strategy by the user. These labels intentionally look into the
future, and are not available to the model during dryrun/live/backtesting.
**Training** refers to the process of feeding individual feature sets into the
model with associated labels with the goal of matching input feature sets to associated labels.
**Train data** is a subset of the historic data which is fed to the model during
training to adjust weights. This data directly influences weight connections in the model.
**Test data** is a subset of the historic data which is used to evaluate the
intermediate performance of the model during training. This data does not
directly influence nodal weights within the model.
## Install prerequisites
The normal Freqtrade install process will ask the user if they wish to install FreqAI dependencies. The user should reply "yes" to this question if they wish to use FreqAI. If the user did not reply yes, they can manually install these dependencies after the install with:
``` bash
pip install -r requirements-freqai.txt
```
!!! Note
Catboost will not be installed on arm devices (raspberry, Mac M1, ARM based VPS, ...), since Catboost does not provide wheels for this platform.
### Usage with docker
For docker users, 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.
## Configuring FreqAI
### Parameter table
The table below will list all configuration parameters available for FreqAI.
Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways.
| Parameter | Description |
|------------|-------------|
| `freqai` | **Required.** The parent dictionary containing all the parameters below for controlling FreqAI. <br> **Datatype:** dictionary.
| `identifier` | **Required.** A unique name for the current model. This can be reused to reload pre-trained models/data. <br> **Datatype:** string.
| `train_period_days` | **Required.** Number of days to use for the training data (width of the sliding window). <br> **Datatype:** positive integer.
| `backtest_period_days` | **Required.** Number of days to inference into the trained model before sliding the window and retraining. This can be fractional days, but beware that the user provided `timerange` will be divided by this number to yield the number of trainings necessary to complete the backtest. <br> **Datatype:** Float.
| `live_retrain_hours` | Frequency of retraining during dry/live runs. Default set to 0, which means it will retrain as often as possible. <br> **Datatype:** Float > 0.
| `follow_mode` | If true, this instance of FreqAI will look for models associated with `identifier` and load those for inferencing. A `follower` will **not** train new models. `False` by default. <br> **Datatype:** boolean.
| `startup_candles` | Number of candles needed for *backtesting only* to ensure all indicators are non NaNs at the start of the first train period. <br> **Datatype:** positive integer.
| `fit_live_predictions_candles` | Computes target (label) statistics from prediction data, instead of from the training data set. Number of candles is the number of historical candles it uses to generate the statistics. <br> **Datatype:** positive integer.
| `purge_old_models` | Tell FreqAI to delete obsolete models. Otherwise, all historic models will remain on disk. Defaults to `False`. <br> **Datatype:** boolean.
| `expiration_hours` | Ask FreqAI to avoid making predictions if a model is more than `expiration_hours` old. Defaults to 0 which means models never expire. <br> **Datatype:** positive integer.
| | **Feature Parameters**
| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples shown [here](#feature-engineering) <br> **Datatype:** dictionary.
| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` will be created for each coin in this list, and that set of features is added to the base asset feature set. <br> **Datatype:** list of assets (strings).
| `include_timeframes` | A list of timeframes that all indicators in `populate_any_indicators` will be created for and added as features to the base asset feature set. <br> **Datatype:** list of timeframes (strings).
| `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `populate_any_indicators`, refer to `templates/FreqaiExampleStrategy.py` for detailed usage. The user can create custom labels, making use of this parameter not. <br> **Datatype:** positive integer.
| `include_shifted_candles` | Parameter used to add a sense of temporal recency to flattened regression type input data. `include_shifted_candles` takes all features, duplicates and shifts them by the number indicated by user. <br> **Datatype:** positive integer.
| `DI_threshold` | Activates the Dissimilarity Index for outlier detection when above 0, explained in detail [here](#removing-outliers-with-the-dissimilarity-index). <br> **Datatype:** positive float (typically below 1).
| `weight_factor` | Used to set weights for training data points according to their recency, see details and a figure of how it works [here](#controlling-the-model-learning-process). <br> **Datatype:** positive float (typically below 1).
| `principal_component_analysis` | Ask FreqAI to automatically reduce the dimensionality of the data set using PCA. <br> **Datatype:** boolean.
| `use_SVM_to_remove_outliers` | Ask FreqAI to train a support vector machine to detect and remove outliers from the training data set as well as from incoming data points. <br> **Datatype:** boolean.
| `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. E.g. `nu` *Very* broadly, is the percentage of data points that should be considered outliers. `shuffle` is by default false to maintain reproducibility. But these and all others can be added/changed in this dictionary. <br> **Datatype:** dictionary.
| `stratify_training_data` | This value is used to indicate the stratification of the data. e.g. 2 would set every 2nd data point into a separate dataset to be pulled from during training/testing. <br> **Datatype:** positive integer.
| `indicator_max_period_candles` | The maximum *period* used in `populate_any_indicators()` for indicator creation. FreqAI uses this information in combination with the maximum timeframe to calculate how many data points it should download so that the first data point does not have a NaN <br> **Datatype:** positive integer.
| `indicator_periods_candles` | A list of integers used to duplicate all indicators according to a set of periods and add them to the feature set. <br> **Datatype:** list of positive integers.
| `use_DBSCAN_to_remove_outliers` | Inactive by default. If true, FreqAI clusters data using DBSCAN to identify and remove outliers from training and prediction data. <br> **Datatype:** float (fraction of 1).
| | **Data split parameters**
| `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) <br> **Datatype:** dictionary.
| `test_size` | Fraction of data that should be used for testing instead of training. <br> **Datatype:** positive float below 1.
| `shuffle` | Shuffle the training data points during training. Typically for time-series forecasting, this is set to False. <br> **Datatype:** boolean.
| | **Model training parameters**
| `model_training_parameters` | A flexible dictionary that includes all parameters available by the user selected library. For example, if the user uses `LightGBMRegressor`, then this dictionary can contain any parameter available by the `LightGBMRegressor` [here](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html). If the user selects a different model, then this dictionary can contain any parameter from that different model. <br> **Datatype:** dictionary.
| `n_estimators` | A common parameter among regressors which sets the number of boosted trees to fit <br> **Datatype:** integer.
| `learning_rate` | A common parameter among regressors which sets the boosting learning rate. <br> **Datatype:** float.
| `n_jobs`, `thread_count`, `task_type` | Different libraries use different parameter names to control the number of threads used for parallel processing or whether or not it is a `task_type` of `gpu` or `cpu`. <br> **Datatype:** float.
| | **Extraneous parameters**
| `keras` | If your model makes use of keras (typical of Tensorflow based prediction models), activate this flag so that the model save/loading follows keras standards. Default value `false` <br> **Datatype:** boolean.
| `conv_width` | The width of a convolutional neural network input tensor. This replaces the need for `shift` 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. Default value, 2 <br> **Datatype:** integer.
### Important FreqAI dataframe key patterns
Here are the values the user can expect to include/use inside the typical strategy dataframe (`df[]`):
| DataFrame Key | Description |
|------------|-------------|
| `df['&*']` | Any dataframe column prepended with `&` in `populate_any_indicators()` is treated as a training target inside FreqAI (typically following the naming convention `&-s*`). These same dataframe columns names are fed back to the user as the predictions. For example, the user wishes to predict the price change in the next 40 candles (similar to `templates/FreqaiExampleStrategy.py`) by setting `df['&-s_close']`. FreqAI makes the predictions and gives them back to the user under the same key (`df['&-s_close']`) to be used in `populate_entry/exit_trend()`. <br> **Datatype:** depends on the output of the model.
| `df['&*_std/mean']` | The standard deviation and mean values of the user defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand rarity of prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` to evaluate how often a particular prediction was observed during training (or historically with `fit_live_predictions_candles`)<br> **Datatype:** float.
| `df['do_predict']` | An indication of an outlier, this return value is integer between -1 and 2 which lets the user understand if the prediction is trustworthy or not. `do_predict==1` means the prediction is trustworthy. If the [Dissimilarity Index](#removing-outliers-with-the-dissimilarity-index) is above the user defined threshold, it will subtract 1 from `do_predict`. If `use_SVM_to_remove_outliers()` is active, then the Support Vector Machine (SVM) may also detect outliers in training and prediction data. In this case, the SVM will also subtract one from `do_predict`. A particular case is when `do_predict == 2`, it means that the model has expired due to `expired_hours`. <br> **Datatype:** integer between -1 and 2.
| `df['DI_values']` | The raw Dissimilarity Index values to give the user a sense of confidence in the prediction. Lower DI means the data point is closer to the trained parameter space. <br> **Datatype:** float.
| `df['%*']` | Any dataframe column prepended with `%` in `populate_any_indicators()` is treated as a training feature inside FreqAI. For example, the user can include the rsi in the training feature set (similar to `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](#building-the-feature-set). <br>**Note**: since the number of features prepended with `%` can multiply very quickly (10s of thousands of features is easily engineered using the multiplictative functionality described in the `feature_parameters` table.) these features are removed from the dataframe upon return from FreqAI. If the user wishes to keep a particular type of feature for plotting purposes, you can prepend it with `%%`. <br> **Datatype:** depends on the output of the model.
### Example config file
The user interface is isolated to the typical config file. A typical FreqAI config setup could include:
```json
"freqai": {
"startup_candles": 10000,
"purge_old_models": true,
"train_period_days": 30,
"backtest_period_days": 7,
"identifier" : "unique-id",
"feature_parameters" : {
"include_timeframes": ["5m","15m","4h"],
"include_corr_pairlist": [
"ETH/USD",
"LINK/USD",
"BNB/USD"
],
"label_period_candles": 24,
"include_shifted_candles": 2,
"weight_factor": 0,
"indicator_max_period_candles": 20,
"indicator_periods_candles": [10, 20]
},
"data_split_parameters" : {
"test_size": 0.25,
"random_state": 42
},
"model_training_parameters" : {
"n_estimators": 100,
"random_state": 42,
"learning_rate": 0.02,
"task_type": "CPU",
},
}
```
### Feature engineering
Features are added by the user inside the `populate_any_indicators()` method of the strategy
by prepending indicators with `%` and labels are added by prepending `&`.
There are some important components/structures that the user *must* include when building their feature set.
Another structure to consider is the location of the labels at the bottom of the example function (below `if set_generalized_indicators:`).
This is where the user will add single features and labels to their feature set to avoid duplication from
various configuration parameters which multiply the feature set such as `include_timeframes`.
```python
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
"""
Function designed to automatically generate, name and merge features
from user indicated timeframes in the configuration file. User controls the indicators
passed to the training/prediction by prepending indicators with `'%-' + coin `
(see convention below). I.e. user should not prepend any supporting metrics
(e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the
model.
:param pair: pair to be used as informative
:param df: strategy dataframe which will receive merges from informatives
:param tf: timeframe of the dataframe which will modify the feature names
:param informative: the dataframe associated with the informative pair
:param coin: the name of the coin which will modify the feature names.
"""
coint = pair.split('/')[0]
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
t = int(t)
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(informative), window=t, stds=2.2
)
informative[f"{coin}bb_lowerband-period_{t}"] = bollinger["lower"]
informative[f"{coin}bb_middleband-period_{t}"] = bollinger["mid"]
informative[f"{coin}bb_upperband-period_{t}"] = bollinger["upper"]
informative[f"%-{coin}bb_width-period_{t}"] = (
informative[f"{coin}bb_upperband-period_{t}"]
- informative[f"{coin}bb_lowerband-period_{t}"]
) / informative[f"{coin}bb_middleband-period_{t}"]
informative[f"%-{coin}close-bb_lower-period_{t}"] = (
informative["close"] / informative[f"{coin}bb_lowerband-period_{t}"]
)
informative[f"%-{coin}relative_volume-period_{t}"] = (
informative["volume"] / informative["volume"].rolling(t).mean()
)
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
# user adds targets here by prepending them with &- (see convention below)
# If user wishes to use multiple targets, a multioutput prediction model
# needs to be used such as templates/CatboostPredictionMultiModel.py
df["&-s_close"] = (
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ df["close"]
- 1
)
return df
```
The user of the present example does not wish to pass the `bb_lowerband` as a feature to the model,
and has therefore not prepended it with `%`. The user does, however, wish to pass `bb_width` to the
model for training/prediction and has therefore prepended it with `%`.
The `include_timeframes` from the example config above are the timeframes (`tf`) of each call to `populate_any_indicators()`
included metric for inclusion in the feature set. In the present case, the user is asking for the
`5m`, `15m`, and `4h` timeframes of the `rsi`, `mfi`, `roc`, and `bb_width` to be included in the feature set.
In addition, the user can ask for each of these features to be included from
informative pairs using the `include_corr_pairlist`. This means that the present feature
set will include all the features from `populate_any_indicators` on all the `include_timeframes` for each of
`ETH/USD`, `LINK/USD`, and `BNB/USD`.
`include_shifted_candles` is another user controlled parameter which indicates the number of previous
candles to include in the present feature set. In other words, `include_shifted_candles: 2`, tells
FreqAI to include the the past 2 candles for each of the features included in the dataset.
In total, the number of features the present user has created is:
length of `include_timeframes` * no. features in `populate_any_indicators()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
$3 * 3 * 3 * 2 * 2 = 108$.
!!! Note
Features **must** be defined in `populate_any_indicators()`. Making features in `populate_indicators()`
will fail in live/dry mode. If the user wishes to add generalized features that are not associated with
a specific pair or timeframe, they should use the following structure inside `populate_any_indicators()`
(as exemplified in `freqtrade/templates/FreqaiExampleStrategy.py`:
```python
def populate_any_indicators(self, metadata, pair, df, tf, informative=None, coin="", set_generalized_indicators=False):
...
# Add generalized indicators here (because in live, it will call only this function to populate
# indicators for retraining). Notice how we ensure not to add them multiple times by associating
# these generalized indicators to the basepair/timeframe
if set_generalized_indicators:
df['%-day_of_week'] = (df["date"].dt.dayofweek + 1) / 7
df['%-hour_of_day'] = (df['date'].dt.hour + 1) / 25
# user adds targets here by prepending them with &- (see convention below)
# If user wishes to use multiple targets, a multioutput prediction model
# needs to be used such as templates/CatboostPredictionMultiModel.py
df["&-s_close"] = (
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ df["close"]
- 1
)
```
(Please see the example script located in `freqtrade/templates/FreqaiExampleStrategy.py` for a full example of `populate_any_indicators()`)
### Deciding the sliding training window and backtesting duration
Users define the backtesting timerange with the typical `--timerange` parameter in the user
configuration file. `train_period_days` is the duration of the sliding training window, while
`backtest_period_days` is the sliding backtesting window, both in number of days (`backtest_period_days` can be
a float to indicate sub daily retraining in live/dry mode). In the present example,
the user is asking FreqAI to use a training period of 30 days and backtest the subsequent 7 days.
This means that if the user sets `--timerange 20210501-20210701`,
FreqAI will train 8 separate models (because the full range comprises 8 weeks),
and then backtest the subsequent week associated with each of the 8 training
data set timerange months. Users can think of this as a "sliding window" which
emulates FreqAI retraining itself once per week in live using the previous
month of data.
In live, the required training data is automatically computed and downloaded. However, in backtesting
the user must manually enter the required number of `startup_candles` in the config. This value
is used to increase the available data to FreqAI and should be sufficient to enable all indicators
to be NaN free at the beginning of the first training timerange. This boils down to identifying the
highest timeframe (`4h` in present example) and the longest indicator period (25 in present example)
and adding this to the `train_period_days`. The units need to be in the base candle time frame:
`startup_candles` = ( 4 hours * 25 max period * 60 minutes/hour + 30 day train_period_days * 1440 minutes per day ) / 5 min (base time frame) = 1488.
!!! Note
In dry/live, this is all precomputed and handled automatically. Thus, `startup_candle` has no influence on dry/live.
!!! Note
Although fractional `backtest_period_days` is allowed, the user should be ware that the `--timerange` is divided by this value to determine the number of models that FreqAI will need to train in order to backtest the full range. For example, if the user wants to set a `--timerange` of 10 days, and asks for a `backtest_period_days` of 0.1, FreqAI will need to train 100 models per pair to complete the full backtest. This is why it is physically impossible to truly backtest FreqAI adaptive training. The best way to fully test a model is to run it dry and let it constantly train. In this case, backtesting would take the exact same amount of time as a dry run.
## Running FreqAI
### Backtesting
The FreqAI backtesting module can be executed with the following command:
```bash
freqtrade backtesting --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel LightGBMRegressor --timerange 20210501-20210701
```
Backtesting mode requires the user to have the data pre-downloaded (unlike dry/live, where FreqAI automatically downloads the necessary data). The user should be careful to consider that the range of the downloaded data is more than the backtesting range. This is because FreqAI needs data prior to the desired backtesting range in order to train a model to be ready to make predictions on the first candle of the user set backtesting range. More details on how to calculate the data download timerange can be found [here](#deciding-the-sliding-training-window-and-backtesting-duration).
If this command has never been executed with the existing config file, then it will train a new model
for each pair, for each backtesting window within the bigger `--timerange`.
!!! Note "Model reuse"
Once the training is completed, the user can execute this again with the same config file and
FreqAI will find the trained models and load them instead of spending time training. This is useful
if the user wants to tweak (or even hyperopt) buy and sell criteria inside the strategy. IF the user
*wants* to retrain a new model with the same config file, then he/she should simply change the `identifier`.
This way, the user can return to using any model they wish by simply changing the `identifier`.
---
### Building a freqai strategy
The FreqAI strategy requires the user to include the following lines of code in the strategy:
```python
def informative_pairs(self):
whitelist_pairs = self.dp.current_whitelist()
corr_pairs = self.config["freqai"]["feature_parameters"]["include_corr_pairlist"]
informative_pairs = []
for tf in self.config["freqai"]["feature_parameters"]["include_timeframes"]:
for pair in whitelist_pairs:
informative_pairs.append((pair, tf))
for pair in corr_pairs:
if pair in whitelist_pairs:
continue # avoid duplication
informative_pairs.append((pair, tf))
return informative_pairs
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# All indicators must be populated by populate_any_indicators() for live functionality
# to work correctly.
# the model will return all labels created by user in `populate_any_indicators`
# (& appended targets), an indication of whether or not the prediction should be accepted,
# the target mean/std values for each of the labels created by user in
# `populate_any_indicators()` for each training period.
dataframe = self.freqai.start(dataframe, metadata, self)
return dataframe
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
"""
Function designed to automatically generate, name and merge features
from user indicated timeframes in the configuration file. User controls the indicators
passed to the training/prediction by prepending indicators with `'%-' + coin `
(see convention below). I.e. user should not prepend any supporting metrics
(e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the
model.
:param pair: pair to be used as informative
:param df: strategy dataframe which will receive merges from informatives
:param tf: timeframe of the dataframe which will modify the feature names
:param informative: the dataframe associated with the informative pair
:param coin: the name of the coin which will modify the feature names.
"""
coin = pair.split('/')[0]
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
t = int(t)
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
# user adds targets here by prepending them with &- (see convention below)
# If user wishes to use multiple targets, a multioutput prediction model
# needs to be used such as templates/CatboostPredictionMultiModel.py
df["&-s_close"] = (
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ df["close"]
- 1
)
return df
```
Notice how the `populate_any_indicators()` is where the user adds their own features and labels ([more information](#feature-engineering)). See a full example at `templates/FreqaiExampleStrategy.py`.
### Setting classifier targets
FreqAI includes a the `CatboostClassifier` via the flag `--freqaimodel CatboostClassifier`. Typically, the user would set the targets using strings:
```python
df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down')
```
### Running the model live
FreqAI can be run dry/live using the following command
```bash
freqtrade trade --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel LightGBMRegressor
```
By default, FreqAI will not find any existing models and will start by training a new one
given the user configuration settings. Following training, it will use that model to make predictions on incoming candles until a new model is available. New models are typically generated as often as possible, with FreqAI managing an internal queue of the pairs to try and keep all models equally "young." FreqAI will always use the newest trained model to make predictions on incoming live data. If users do not want FreqAI to retrain new models as often as possible, they can set `live_retrain_hours` to tell FreqAI to wait at least that number of hours before retraining a new model. Additionally, users can set `expired_hours` to tell FreqAI to avoid making predictions on models aged over this number of hours.
If the user wishes to start dry/live from a backtested saved model, the user only needs to reuse
the same `identifier` parameter
```json
"freqai": {
"identifier": "example",
"live_retrain_hours": 1
}
```
In this case, although FreqAI will initiate with a
pre-trained model, it will still check to see how much time has elapsed since the model was trained,
and if a full `live_retrain_hours` has elapsed since the end of the loaded model, FreqAI will self retrain.
## Data analysis techniques
### Controlling the model learning process
Model training parameters are unique to the ML library used by the user. FreqAI allows users to set any parameter for any library using the `model_training_parameters` dictionary in the user configuration file. The example configuration files show some of the example parameters associated with `Catboost` and `LightGBM`, but users can add any parameters available in those libraries.
Data split parameters are defined in `data_split_parameters` which can be any parameters associated with `Sklearn`'s `train_test_split()` function. FreqAI includes some additional parameters such `weight_factor` which allows the user to weight more recent data more strongly
than past data via an exponential function:
$$ W_i = \exp(\frac{-i}{\alpha*n}) $$
where $W_i$ is the weight of data point $i$ in a total set of $n$ data points.
![weight-factor](assets/weights_factor.png)
`train_test_split()` has a parameters called `shuffle`, which users also have access to in FreqAI, that allows them to keep the data unshuffled. This is particularly useful to avoid biasing training with temporally auto-correlated data.
Finally, `label_period_candles` defines the offset used for the `labels`. In the present example,
the user is asking for `labels` that are 24 candles in the future.
### Removing outliers with the Dissimilarity Index
The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each
prediction by the model. To do so, FreqAI measures the distance between each training
data point and all other training data points:
$$ d_{ab} = \sqrt{\sum_{j=1}^p(X_{a,j}-X_{b,j})^2} $$
where $d_{ab}$ is the distance between the normalized points $a$ and $b$. $p$
is the number of features i.e. the length of the vector $X$.
The characteristic distance, $\overline{d}$ for a set of training data points is simply the mean
of the average distances:
$$ \overline{d} = \sum_{a=1}^n(\sum_{b=1}^n(d_{ab}/n)/n) $$
$\overline{d}$ quantifies the spread of the training data, which is compared to
the distance between the new prediction feature vectors, $X_k$ and all the training
data:
$$ d_k = \arg \min d_{k,i} $$
which enables the estimation of a Dissimilarity Index:
$$ DI_k = d_k/\overline{d} $$
Equity and crypto markets suffer from a high level of non-patterned noise in the
form of outlier data points. The dissimilarity index allows predictions which
are outliers and not existent in the model feature space, to be thrown out due
to low levels of certainty. Activating the Dissimilarity Index can be achieved with:
```json
"freqai": {
"feature_parameters" : {
"DI_threshold": 1
}
}
```
The user can tweak the DI with `DI_threshold` to increase or decrease the extrapolation of the trained model.
### Reducing data dimensionality with Principal Component Analysis
Users can reduce the dimensionality of their features by activating the `principal_component_analysis`:
```json
"freqai": {
"feature_parameters" : {
"principal_component_analysis": true
}
}
```
Which will perform PCA on the features and reduce the dimensionality of the data so that the explained
variance of the data set is >= 0.999.
### Removing outliers using a Support Vector Machine (SVM)
The user can tell FreqAI to remove outlier data points from the training/test data sets by setting:
```json
"freqai": {
"feature_parameters" : {
"use_SVM_to_remove_outliers": true
}
}
```
FreqAI will train an SVM on the training data (or components if the user activated
`principal_component_analysis`) and remove any data point that it deems to be sitting beyond the feature space.
### Clustering the training data and removing outliers with DBSCAN
The user can configure FreqAI to use DBSCAN to cluster training data and remove outliers from the training data set. The user activates `use_DBSCAN_to_remove_outliers` to cluster training data for identification of outliers. Also used to detect incoming outliers for prediction data points.
```json
"freqai": {
"feature_parameters" : {
"use_DBSCAN_to_remove_outliers": true
}
}
```
### Stratifying the data
The user can stratify the training/testing data using:
```json
"freqai": {
"feature_parameters" : {
"stratify_training_data": 3
}
}
```
which will split the data chronologically so that every Xth data points is a testing data point. In the
present example, the user is asking for every third data point in the dataframe to be used for
testing, the other points are used for training.
## Setting up a follower
The user can define:
```json
"freqai": {
"follow_mode": true,
"identifier": "example"
}
```
to indicate to the bot that it should not train models, but instead should look for models trained
by a leader with the same `identifier`. In this example, the user has a leader bot with the
`identifier: "example"` already running or launching simultaneously as the present follower.
The follower will load models created by the leader and inference them to obtain predictions.
## Purging old model data
FreqAI stores new model files each time it retrains. These files become obsolete as new models
are trained and FreqAI adapts to the new market conditions. Users planning to leave FreqAI running
for extended periods of time with high frequency retraining should set `purge_old_models` in their
config:
```json
"freqai": {
"purge_old_models": true,
}
```
which will automatically purge all models older than the two most recently trained ones.
## Defining model expirations
During dry/live, FreqAI trains each pair sequentially (on separate threads/GPU from the main
Freqtrade bot). This means there is always an age discrepancy between models. If a user is training
on 50 pairs, and each pair requires 5 minutes to train, the oldest model will be over 4 hours old.
This may be undesirable if the characteristic time scale (read trade duration target) for a strategy
is much less than 4 hours. The user can decide to only make trade entries if the model is less than
a certain number of hours in age by setting the `expiration_hours` in the config file:
```json
"freqai": {
"expiration_hours": 0.5,
}
```
In the present example, the user will only allow predictions on models that are less than 1/2 hours
old.
## Choosing the calculation of the `target_roi`
As shown in `templates/FreqaiExampleStrategy.py`, the `target_roi` is based on two metrics computed
by FreqAI: `label_mean` and `label_std`. These are the statistics associated with the labels used
*during the most recent training*.
This allows the model to know what magnitude of a target to be expecting since it is directly stemming from the training data.
By default, FreqAI computes this based on training data and it assumes the labels are Gaussian distributed.
These are big assumptions that the user should consider when creating their labels. If the user wants to consider the population
of *historical predictions* for creating the dynamic target instead of the trained labels, the user
can do so by setting `fit_live_prediction_candles` to the number of historical prediction candles
the user wishes to use to generate target statistics.
```json
"freqai": {
"fit_live_prediction_candles": 300,
}
```
If the user sets this value, FreqAI will initially use the predictions from the training data set
and then subsequently begin introducing real prediction data as it is generated. FreqAI will save
this historical data to be reloaded if the user stops and restarts with the same `identifier`.
## Extra returns per train
Users may find that there are some important metrics that they'd like to return to the strategy at the end of each retrain.
Users can include these metrics by assigning them to `dk.data['extra_returns_per_train']['my_new_value'] = XYZ` inside their custom prediction
model class. FreqAI takes the `my_new_value` assigned in this dictionary and expands it to fit the return dataframe to the strategy.
The user can then use the value in the strategy with `dataframe['my_new_value']`. An example of how this is already used in FreqAI is
the `&*_mean` and `&*_std` values, which indicate the mean and standard deviation of that particular label during the most recent training.
Another example is shown below if the user wants to use live metrics from the trade database.
The user needs to set the standard dictionary in the config so FreqAI can return proper dataframe shapes:
```json
"freqai": {
"extra_returns_per_train": {"total_profit": 4}
}
```
These values will likely be overridden by the user prediction model, but in the case where the user model has yet to set them, or needs
a default initial value - this is the value that will be returned.
## Building an IFreqaiModel
FreqAI has multiple example prediction model based libraries such as `Catboost` regression (`freqai/prediction_models/CatboostRegressor.py`) and `LightGBM` regression.
However, users can customize and create their own prediction models using the `IFreqaiModel` class.
Users are encouraged to inherit `train()` and `predict()` to let them customize various aspects of their training procedures.
## Additional information
### Common pitfalls
FreqAI cannot be combined with `VolumePairlists` (or any pairlist filter that adds and removes pairs dynamically).
This is for performance reasons - FreqAI relies on making quick predictions/retrains. To do this effectively,
it needs to download all the training data at the beginning of a dry/live instance. FreqAI stores and appends
new candles automatically for future retrains. But this means that if new pairs arrive later in the dry run due
to a volume pairlist, it will not have the data ready. FreqAI does work, however, with the `ShufflePairlist`.
### Feature normalization
The feature set created by the user is automatically normalized to the training data only.
This includes all test data and unseen prediction data (dry/live/backtest).
### File structure
`user_data_dir/models/` contains all the data associated with the trainings and backtests.
This file structure is heavily controlled and read by the `FreqaiDataKitchen()`
and should therefore not be modified.
## Credits
FreqAI was developed by a group of individuals who all contributed specific skillsets to the project.
Conception and software development:
Robert Caulk @robcaulk
Theoretical brainstorming:
Elin Törnquist @thorntwig
Code review, software architecture brainstorming:
@xmatthias
Beta testing and bug reporting:
@bloodhunter4rc, Salah Lamkadem @ikonx, @ken11o2, @longyu, @paranoidandy, @smidelis, @smarm
Juha Nykänen @suikula, Wagner Costa @wagnercosta

View File

@@ -326,16 +326,6 @@ python3 -m pip install --upgrade pip
python3 -m pip install -e . python3 -m pip install -e .
``` ```
Patch conda libta-lib (Linux only)
```bash
# Ensure that the environment is active!
conda activate freqtrade-conda
cd build_helpers
bash install_ta-lib.sh ${CONDA_PREFIX} nosudo
```
### Congratulations ### Congratulations
[You are ready](#you-are-ready), and run the bot [You are ready](#you-are-ready), and run the bot

View File

@@ -1,6 +1,6 @@
markdown==3.3.7 markdown==3.3.7
mkdocs==1.3.1 mkdocs==1.3.1
mkdocs-material==8.4.0 mkdocs-material==8.3.9
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==9.5 pymdown-extensions==9.5
jinja2==3.1.2 jinja2==3.1.2

View File

@@ -623,13 +623,12 @@ class AwesomeStrategy(IStrategy):
!!! Warning !!! Warning
`confirm_trade_exit()` can prevent stoploss exits, causing significant losses as this would ignore stoploss exits. `confirm_trade_exit()` can prevent stoploss exits, causing significant losses as this would ignore stoploss exits.
`confirm_trade_exit()` will not be called for Liquidations - as liquidations are forced by the exchange, and therefore cannot be rejected.
## Adjust trade position ## Adjust trade position
The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in the strategy. The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in the strategy.
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled. For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions. `adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging).
`max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys. `max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys.
@@ -637,13 +636,10 @@ The strategy is expected to return a stake_amount (in stake currency) between `m
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored. If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`. Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
This callback is **not** called when there is an open order (either buy or sell) waiting for execution. This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`.
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
Additional Buys are ignored once you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits. Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position, no matter if it's a long or short trade. Modifications to leverage are not possible.
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. Modifications to leverage are not possible.
!!! Note "About stake size" !!! Note "About stake size"
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
@@ -652,12 +648,12 @@ Position adjustments will always be applied in the direction of the trade, so a
!!! Warning !!! Warning
Stoploss is still calculated from the initial opening price, not averaged price. Stoploss is still calculated from the initial opening price, not averaged price.
Regular stoploss rules still apply (cannot move down).
!!! Warning "/stopbuy"
While `/stopbuy` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades. While `/stopbuy` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades.
!!! Warning "Backtesting" !!! Warning "Backtesting"
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected. During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so performance will be affected.
``` python ``` python
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@@ -678,7 +674,7 @@ class DigDeeperStrategy(IStrategy):
max_dca_multiplier = 5.5 max_dca_multiplier = 5.5
# This is called when placing the initial order (opening trade) # This is called when placing the initial order (opening trade)
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: Optional[float], max_stake: float, proposed_stake: float, min_stake: Optional[float], max_stake: float,
leverage: float, entry_tag: Optional[str], side: str, leverage: float, entry_tag: Optional[str], side: str,
**kwargs) -> float: **kwargs) -> float:
@@ -688,41 +684,22 @@ class DigDeeperStrategy(IStrategy):
return proposed_stake / self.max_dca_multiplier return proposed_stake / self.max_dca_multiplier
def adjust_trade_position(self, trade: Trade, current_time: datetime, def adjust_trade_position(self, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, current_rate: float, current_profit: float, min_stake: Optional[float],
min_stake: Optional[float], max_stake: float, max_stake: float, **kwargs):
current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float,
**kwargs) -> Optional[float]:
""" """
Custom trade adjustment logic, returning the stake amount that a trade should be Custom trade adjustment logic, returning the stake amount that a trade should be increased.
increased or decreased. This means extra buy orders with additional fees.
This means extra buy or sell orders with additional fees.
Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns None
:param trade: trade object. :param trade: trade object.
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param current_rate: Current buy rate. :param current_rate: Current buy rate.
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate.
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits) :param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Maximum stake allowed (either through balance, or by exchange limits). :param max_stake: Balance available for trading.
:param current_entry_rate: Current rate using entry pricing.
:param current_exit_rate: Current rate using exit pricing.
:param current_entry_profit: Current profit using entry pricing.
:param current_exit_profit: Current profit using exit pricing.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: Stake amount to adjust your trade, :return float: Stake amount to adjust your trade
Positive values to increase position, Negative values to decrease position.
Return None for no action.
""" """
if current_profit > 0.05 and trade.nr_of_successful_exits == 0:
# Take half of the profit at +5%
return -(trade.stake_amount / 2)
if current_profit > -0.05: if current_profit > -0.05:
return None return None
@@ -757,25 +734,6 @@ class DigDeeperStrategy(IStrategy):
``` ```
### Position adjust calculations
* Entry rates are calculated using weighted averages.
* Exits will not influence the average entry rate.
* Partial exit relative profit is relative to the average entry price at this point.
* Final exit relative profit is calculated based on the total invested capital. (See example below)
??? example "Calculation example"
*This example assumes 0 fees for simplicity, and a long position on an imaginary coin.*
* Buy 100@8\$
* Buy 100@9\$ -> Avg price: 8.5\$
* Sell 100@10\$ -> Avg price: 8.5\$, realized profit 150\$, 17.65%
* Buy 150@11\$ -> Avg price: 10\$, realized profit 150\$, 17.65%
* Sell 100@12\$ -> Avg price: 10\$, total realized profit 350\$, 20%
* Sell 150@14\$ -> Avg price: 10\$, total realized profit 950\$, 40%
The total profit for this trade was 950$ on a 3350$ investment (`100@8$ + 100@9$ + 150@11$`). As such - the final relative profit is 28.35% (`950 / 3350`).
## Adjust Entry Price ## Adjust Entry Price
The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles. The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles.

View File

@@ -617,7 +617,8 @@ Please always check the mode of operation to select the correct method to get da
### *available_pairs* ### *available_pairs*
``` python ``` python
for pair, timeframe in self.dp.available_pairs: if self.dp:
for pair, timeframe in self.dp.available_pairs:
print(f"available {pair}, {timeframe}") print(f"available {pair}, {timeframe}")
``` ```
@@ -629,7 +630,7 @@ The strategy might look something like this:
*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to buy and sell.* *Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to buy and sell.*
Due to the limited available data, it's very difficult to resample `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500-1000 candles which effectively gives us around 1.74 daily candles. We need 14 days at least! Due to the limited available data, it's very difficult to resample `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least!
Since we can't resample the data we will have to use an informative pair; and since the whitelist will be dynamic we don't know which pair(s) to use. Since we can't resample the data we will have to use an informative pair; and since the whitelist will be dynamic we don't know which pair(s) to use.
@@ -652,8 +653,9 @@ This is where calling `self.dp.current_whitelist()` comes in handy.
``` python ``` python
# fetch live / historical candle (OHLCV) data for the first informative pair # fetch live / historical candle (OHLCV) data for the first informative pair
inf_pair, inf_timeframe = self.informative_pairs()[0] if self.dp:
informative = self.dp.get_pair_dataframe(pair=inf_pair, inf_pair, inf_timeframe = self.informative_pairs()[0]
informative = self.dp.get_pair_dataframe(pair=inf_pair,
timeframe=inf_timeframe) timeframe=inf_timeframe)
``` ```
@@ -669,7 +671,8 @@ It can also be used in specific callbacks to get the signal that caused the acti
``` python ``` python
# fetch current dataframe # fetch current dataframe
if self.dp.runmode.value in ('live', 'dry_run'): if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'):
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'], dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'],
timeframe=self.timeframe) timeframe=self.timeframe)
``` ```
@@ -681,7 +684,8 @@ if self.dp.runmode.value in ('live', 'dry_run'):
### *orderbook(pair, maximum)* ### *orderbook(pair, maximum)*
``` python ``` python
if self.dp.runmode.value in ('live', 'dry_run'): if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'):
ob = self.dp.orderbook(metadata['pair'], 1) ob = self.dp.orderbook(metadata['pair'], 1)
dataframe['best_bid'] = ob['bids'][0][0] dataframe['best_bid'] = ob['bids'][0][0]
dataframe['best_ask'] = ob['asks'][0][0] dataframe['best_ask'] = ob['asks'][0][0]
@@ -713,7 +717,8 @@ Therefore, using `ob['bids'][0][0]` as demonstrated above will result in using t
### *ticker(pair)* ### *ticker(pair)*
``` python ``` python
if self.dp.runmode.value in ('live', 'dry_run'): if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'):
ticker = self.dp.ticker(metadata['pair']) ticker = self.dp.ticker(metadata['pair'])
dataframe['last_price'] = ticker['last'] dataframe['last_price'] = ticker['last']
dataframe['volume24h'] = ticker['quoteVolume'] dataframe['volume24h'] = ticker['quoteVolume']
@@ -727,24 +732,7 @@ if self.dp.runmode.value in ('live', 'dry_run'):
data returned from the exchange and add appropriate error handling / defaults. data returned from the exchange and add appropriate error handling / defaults.
!!! Warning "Warning about backtesting" !!! Warning "Warning about backtesting"
This method will always return up-to-date values - so usage during backtesting / hyperopt without runmode checks will lead to wrong results. This method will always return up-to-date values - so usage during backtesting / hyperopt will lead to wrong results.
### Send Notification
The dataprovider `.send_msg()` function allows you to send custom notifications from your strategy.
Identical notifications will only be sent once per candle, unless the 2nd argument (`always_send`) is set to True.
``` python
self.dp.send_msg(f"{metadata['pair']} just got hot!")
# Force send this notification, avoid caching (Please read warning below!)
self.dp.send_msg(f"{metadata['pair']} just got hot!", always_send=True)
```
Notifications will only be sent in trading modes (Live/Dry-run) - so this method can be called without conditions for backtesting.
!!! Warning "Spamming"
You can spam yourself pretty good by setting `always_send=True` in this method. Use this with great care and only in conditions you know will not happen throughout a candle to avoid a message every 5 seconds.
### Complete Data-provider sample ### Complete Data-provider sample

View File

@@ -14,7 +14,7 @@ from freqtrade.configuration import Configuration
# Initialize empty configuration object # Initialize empty configuration object
config = Configuration.from_files([]) config = Configuration.from_files([])
# Optionally (recommended), use existing configuration file # Optionally, use existing configuration file
# config = Configuration.from_files(["config.json"]) # config = Configuration.from_files(["config.json"])
# Define some constants # Define some constants
@@ -22,7 +22,7 @@ config["timeframe"] = "5m"
# Name of the strategy class # Name of the strategy class
config["strategy"] = "SampleStrategy" config["strategy"] = "SampleStrategy"
# Location of the data # Location of the data
data_location = config['datadir'] data_location = Path(config['user_data_dir'], 'data', 'binance')
# Pair to analyze - Only use one pair here # Pair to analyze - Only use one pair here
pair = "BTC/USDT" pair = "BTC/USDT"
``` ```

View File

@@ -18,7 +18,7 @@ Note : `forcesell`, `forcebuy`, `emergencysell` are changed to `force_exit`, `fo
* [`check_buy_timeout()` -> `check_entry_timeout()`](#custom_entry_timeout) * [`check_buy_timeout()` -> `check_entry_timeout()`](#custom_entry_timeout)
* [`check_sell_timeout()` -> `check_exit_timeout()`](#custom_entry_timeout) * [`check_sell_timeout()` -> `check_exit_timeout()`](#custom_entry_timeout)
* New `side` argument to callbacks without trade object * New `side` argument to callbacks without trade object
* [`custom_stake_amount`](#custom_stake_amount) * [`custom_stake_amount`](#custom-stake-amount)
* [`confirm_trade_entry`](#confirm_trade_entry) * [`confirm_trade_entry`](#confirm_trade_entry)
* [`custom_entry_price`](#custom_entry_price) * [`custom_entry_price`](#custom_entry_price)
* [Changed argument name in `confirm_trade_exit`](#confirm_trade_exit) * [Changed argument name in `confirm_trade_exit`](#confirm_trade_exit)
@@ -192,7 +192,7 @@ class AwesomeStrategy(IStrategy):
return False return False
``` ```
### `custom_stake_amount` ### Custom-stake-amount
New string argument `side` - which can be either `"long"` or `"short"`. New string argument `side` - which can be either `"long"` or `"short"`.

View File

@@ -98,7 +98,6 @@ Example configuration showing the different settings:
"exit_fill": "off", "exit_fill": "off",
"protection_trigger": "off", "protection_trigger": "off",
"protection_trigger_global": "on", "protection_trigger_global": "on",
"strategy_msg": "off",
"show_candle": "off" "show_candle": "off"
}, },
"reload": true, "reload": true,
@@ -110,8 +109,7 @@ Example configuration showing the different settings:
`exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange. `exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
`*_fill` notifications are off by default and must be explicitly enabled. `*_fill` notifications are off by default and must be explicitly enabled.
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered. `protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
`strategy_msg` - Receive notifications from the strategy, sent via `self.dp.send_msg()` from the strategy [more details](strategy-customization.md#send-notification). `show_candle` - show candle values as part of entry/exit messages. Only possible value is "ohlc".
`show_candle` - show candle values as part of entry/exit messages. Only possible values are `"ohlc"` or `"off"`.
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
`reload` allows you to disable reload-buttons on selected messages. `reload` allows you to disable reload-buttons on selected messages.
@@ -187,7 +185,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/stats` | Shows Wins / losses by Exit reason as well as Avg. holding durations for buys and sells | `/stats` | Shows Wins / losses by Exit reason as well as Avg. holding durations for buys and sells
| `/exits` | Shows Wins / losses by Exit reason as well as Avg. holding durations for buys and sells | `/exits` | Shows Wins / losses by Exit reason as well as Avg. holding durations for buys and sells
| `/entries` | Shows Wins / losses by Exit reason as well as Avg. holding durations for buys and sells | `/entries` | Shows Wins / losses by Exit reason as well as Avg. holding durations for buys and sells
| `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing. | `/whitelist` | Show the current whitelist
| `/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 | `/help` | Show help message

View File

@@ -611,26 +611,6 @@ Common arguments:
``` ```
### Webserver mode - docker
You can also use webserver mode via docker.
Starting a one-off container requires the configuration of the port explicitly, as ports are not exposed by default.
You can use `docker-compose run --rm -p 127.0.0.1:8080:8080 freqtrade webserver` to start a one-off container that'll be removed once you stop it. This assumes that port 8080 is still available and no other bot is running on that port.
Alternatively, you can reconfigure the docker-compose file to have the command updated:
``` yml
command: >
webserver
--config /freqtrade/user_data/config.json
```
You can now use `docker-compose up` to start the webserver.
This assumes that the configuration has a webserver enabled and configured for docker (listening port = `0.0.0.0`).
!!! Tip
Don't forget to reset the command back to the trade command if you want to start a live or dry-run bot.
## Show previous Backtest results ## Show previous Backtest results
Allows you to show previous backtest results. Allows you to show previous backtest results.

View File

@@ -9,7 +9,6 @@ dependencies:
- pandas - pandas
- pip - pip
- py-find-1st
- aiohttp - aiohttp
- SQLAlchemy - SQLAlchemy
- python-telegram-bot - python-telegram-bot
@@ -65,7 +64,7 @@ dependencies:
- pip: - pip:
- pycoingecko - pycoingecko
# - py_find_1st - py_find_1st
- tables - tables
- pytest-random-order - pytest-random-order
- ccxt - ccxt

View File

@@ -1,5 +1,5 @@
""" Freqtrade bot """ """ Freqtrade bot """
__version__ = '2022.8.dev' __version__ = '2022.7'
if 'dev' in __version__: if 'dev' in __version__:
try: try:

View File

@@ -12,8 +12,7 @@ from freqtrade.constants import DEFAULT_CONFIG
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"] ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
ARGS_STRATEGY = ["strategy", "strategy_path", "recursive_strategy_search", "freqaimodel", ARGS_STRATEGY = ["strategy", "strategy_path", "recursive_strategy_search"]
"freqaimodel_path"]
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"]

View File

@@ -67,7 +67,7 @@ def ask_user_config() -> Dict[str, Any]:
"type": "text", "type": "text",
"name": "stake_amount", "name": "stake_amount",
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):", "message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
"default": "unlimited", "default": "100",
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val), "validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"' "filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
if val == UNLIMITED_STAKE_AMOUNT if val == UNLIMITED_STAKE_AMOUNT
@@ -164,7 +164,7 @@ def ask_user_config() -> Dict[str, Any]:
"when": lambda x: x['telegram'] "when": lambda x: x['telegram']
}, },
{ {
"type": "password", "type": "text",
"name": "telegram_chat_id", "name": "telegram_chat_id",
"message": "Insert Telegram chat id", "message": "Insert Telegram chat id",
"when": lambda x: x['telegram'] "when": lambda x: x['telegram']
@@ -191,7 +191,7 @@ def ask_user_config() -> Dict[str, Any]:
"when": lambda x: x['api_server'] "when": lambda x: x['api_server']
}, },
{ {
"type": "password", "type": "text",
"name": "api_server_password", "name": "api_server_password",
"message": "Insert api-server password", "message": "Insert api-server password",
"when": lambda x: x['api_server'] "when": lambda x: x['api_server']

View File

@@ -647,14 +647,4 @@ AVAILABLE_CLI_OPTIONS = {
nargs='+', nargs='+',
default=[], default=[],
), ),
"freqaimodel": Arg(
'--freqaimodel',
help='Specify a custom freqaimodels.',
metavar='NAME',
),
"freqaimodel_path": Arg(
'--freqaimodel-path',
help='Specify additional lookup path for freqaimodels.',
metavar='PATH',
),
} }

View File

@@ -12,7 +12,7 @@ from freqtrade.enums import CandleType, RunMode, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.exchange.exchange import market_is_active from freqtrade.exchange.exchange import market_is_active
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
@@ -50,8 +50,7 @@ def start_download_data(args: Dict[str, Any]) -> None:
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
markets = [p for p, m in exchange.markets.items() if market_is_active(m) markets = [p for p, m in exchange.markets.items() if market_is_active(m)
or config.get('include_inactive')] or config.get('include_inactive')]
expanded_pairs = expand_pairlist(config['pairs'], markets)
expanded_pairs = dynamic_expand_pairlist(config, markets)
# Manual validations of relevant settings # Manual validations of relevant settings
if not config['exchange'].get('skip_pair_validation', False): if not config['exchange'].get('skip_pair_validation', False):

View File

@@ -4,4 +4,5 @@ from freqtrade.configuration.check_exchange import check_exchange
from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.config_setup import setup_utils_configuration
from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.config_validation import validate_config_consistency
from freqtrade.configuration.configuration import Configuration from freqtrade.configuration.configuration import Configuration
from freqtrade.configuration.PeriodicCache import PeriodicCache
from freqtrade.configuration.timerange import TimeRange from freqtrade.configuration.timerange import TimeRange

View File

@@ -97,8 +97,6 @@ class Configuration:
self._process_analyze_options(config) self._process_analyze_options(config)
self._process_freqai_options(config)
# Check if the exchange set by the user is supported # Check if the exchange set by the user is supported
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
@@ -463,16 +461,6 @@ class Configuration:
config.update({'runmode': self.runmode}) config.update({'runmode': self.runmode})
def _process_freqai_options(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='freqaimodel',
logstring='Using freqaimodel class name: {}')
self._args_to_config(config, argname='freqaimodel_path',
logstring='Using freqaimodel path: {}')
return
def _args_to_config(self, config: Dict[str, Any], argname: str, def _args_to_config(self, config: Dict[str, Any], argname: str,
logstring: str, logfun: Optional[Callable] = None, logstring: str, logfun: Optional[Callable] = None,
deprecated_msg: Optional[str] = None) -> None: deprecated_msg: Optional[str] = None) -> None:

View File

@@ -55,7 +55,6 @@ FTHYPT_FILEVERSION = 'fthypt_fileversion'
USERPATH_HYPEROPTS = 'hyperopts' USERPATH_HYPEROPTS = 'hyperopts'
USERPATH_STRATEGIES = 'strategies' USERPATH_STRATEGIES = 'strategies'
USERPATH_NOTEBOOKS = 'notebooks' USERPATH_NOTEBOOKS = 'notebooks'
USERPATH_FREQAIMODELS = 'freqaimodels'
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw'] WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
@@ -241,7 +240,6 @@ CONF_SCHEMA = {
}, },
'exchange': {'$ref': '#/definitions/exchange'}, 'exchange': {'$ref': '#/definitions/exchange'},
'edge': {'$ref': '#/definitions/edge'}, 'edge': {'$ref': '#/definitions/edge'},
'freqai': {'$ref': '#/definitions/freqai'},
'experimental': { 'experimental': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
@@ -319,10 +317,6 @@ CONF_SCHEMA = {
'type': 'string', 'type': 'string',
'enum': ['off', 'ohlc'], 'enum': ['off', 'ohlc'],
}, },
'strategy_msg': {
'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS,
},
} }
}, },
'reload': {'type': 'boolean'}, 'reload': {'type': 'boolean'},
@@ -482,61 +476,8 @@ CONF_SCHEMA = {
'remove_pumps': {'type': 'boolean'} 'remove_pumps': {'type': 'boolean'}
}, },
'required': ['process_throttle_secs', 'allowed_risk'] 'required': ['process_throttle_secs', 'allowed_risk']
},
"freqai": {
"type": "object",
"properties": {
"enabled": {"type": "boolean", "default": False},
"keras": {"type": "boolean", "default": False},
"conv_width": {"type": "integer", "default": 2},
"train_period_days": {"type": "integer", "default": 0},
"backtest_period_days": {"type": "number", "default": 7},
"identifier": {"type": "string", "default": "example"},
"feature_parameters": {
"type": "object",
"properties": {
"include_corr_pairlist": {"type": "array"},
"include_timeframes": {"type": "array"},
"label_period_candles": {"type": "integer"},
"include_shifted_candles": {"type": "integer", "default": 0},
"DI_threshold": {"type": "number", "default": 0},
"weight_factor": {"type": "number", "default": 0},
"principal_component_analysis": {"type": "boolean", "default": False},
"use_SVM_to_remove_outliers": {"type": "boolean", "default": False},
"svm_params": {"type": "object",
"properties": {
"shuffle": {"type": "boolean", "default": False},
"nu": {"type": "number", "default": 0.1}
},
} }
}, },
"required": ["include_timeframes", "include_corr_pairlist", ]
},
"data_split_parameters": {
"type": "object",
"properties": {
"test_size": {"type": "number"},
"random_state": {"type": "integer"},
},
},
"model_training_parameters": {
"type": "object",
"properties": {
"n_estimators": {"type": "integer", "default": 1000}
},
},
},
"required": [
"enabled",
"train_period_days",
"backtest_period_days",
"identifier",
"feature_parameters",
"data_split_parameters",
"model_training_parameters"
]
},
},
} }
SCHEMA_TRADE_REQUIRED = [ SCHEMA_TRADE_REQUIRED = [

View File

@@ -5,7 +5,6 @@ including ticker and orderbook data, live and historical candle (OHLCV) data
Common Interface for bot and strategy to access data. Common Interface for bot and strategy to access data.
""" """
import logging import logging
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
@@ -17,7 +16,6 @@ from freqtrade.data.history import load_pair_history
from freqtrade.enums import CandleType, RunMode from freqtrade.enums import CandleType, 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.util import PeriodicCache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -35,10 +33,6 @@ class DataProvider:
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {} self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
self.__slice_index: Optional[int] = None self.__slice_index: Optional[int] = None
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {} self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
self._msg_queue: deque = deque()
self.__msg_cache = PeriodicCache(
maxsize=1000, ttl=timeframe_to_seconds(self._config.get('timeframe', '1h')))
def _set_dataframe_max_index(self, limit_index: int): def _set_dataframe_max_index(self, limit_index: int):
""" """
@@ -271,20 +265,3 @@ class DataProvider:
if self._exchange is None: if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION) raise OperationalException(NO_EXCHANGE_EXCEPTION)
return self._exchange.fetch_l2_order_book(pair, maximum) return self._exchange.fetch_l2_order_book(pair, maximum)
def send_msg(self, message: str, *, always_send: bool = False) -> None:
"""
Send custom RPC Notifications from your bot.
Will not send any bot in modes other than Dry-run or Live.
:param message: Message to be sent. Must be below 4096.
:param always_send: If False, will send the message only once per candle, and surpress
identical messages.
Careful as this can end up spaming your chat.
Defaults to False
"""
if self.runmode not in (RunMode.DRY_RUN, RunMode.LIVE):
return
if always_send or message not in self.__msg_cache:
self._msg_queue.append(message)
self.__msg_cache[message] = True

View File

@@ -7,8 +7,9 @@ import numpy as np
import pandas as pd import pandas as pd
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS,
from freqtrade.enums import CandleType ListPairsWithTimeframes, TradeList)
from freqtrade.enums import CandleType, TradingMode
from .idatahandler import IDataHandler from .idatahandler import IDataHandler
@@ -20,6 +21,29 @@ class HDF5DataHandler(IDataHandler):
_columns = DEFAULT_DATAFRAME_COLUMNS _columns = DEFAULT_DATAFRAME_COLUMNS
@classmethod
def ohlcv_get_available_data(
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
"""
Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files
:param trading_mode: trading-mode to be used
:return: List of Tuples of (pair, timeframe)
"""
if trading_mode == TradingMode.FUTURES:
datadir = datadir.joinpath('futures')
_tmp = [
re.search(
cls._OHLCV_REGEX, p.name
) for p in datadir.glob("*.h5")
]
return [
(
cls.rebuild_pair_from_filename(match[1]),
cls.rebuild_timeframe_from_filename(match[2]),
CandleType.from_string(match[3])
) for match in _tmp if match and len(match.groups()) > 1]
@classmethod @classmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]: def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
""" """

View File

@@ -56,7 +56,7 @@ def load_pair_history(pair: str,
fill_missing=fill_up_missing, fill_missing=fill_up_missing,
drop_incomplete=drop_incomplete, drop_incomplete=drop_incomplete,
startup_candles=startup_candles, startup_candles=startup_candles,
candle_type=candle_type, candle_type=candle_type
) )
@@ -97,15 +97,14 @@ def load_data(datadir: Path,
fill_up_missing=fill_up_missing, fill_up_missing=fill_up_missing,
startup_candles=startup_candles, startup_candles=startup_candles,
data_handler=data_handler, data_handler=data_handler,
candle_type=candle_type, candle_type=candle_type
) )
if not hist.empty: if not hist.empty:
result[pair] = hist result[pair] = hist
else: else:
if candle_type is CandleType.FUNDING_RATE and user_futures_funding_rate is not None: if candle_type is CandleType.FUNDING_RATE and user_futures_funding_rate is not None:
logger.warn(f"{pair} using user specified [{user_futures_funding_rate}]") logger.warn(f"{pair} using user specified [{user_futures_funding_rate}]")
elif candle_type not in (CandleType.SPOT, CandleType.FUTURES): result[pair] = DataFrame(columns=["open", "close", "high", "low", "volume"])
result[pair] = DataFrame(columns=["date", "open", "close", "high", "low", "volume"])
if fail_without_data and not result: if fail_without_data and not result:
raise OperationalException("No data found. Terminating.") raise OperationalException("No data found. Terminating.")

View File

@@ -39,26 +39,15 @@ class IDataHandler(ABC):
raise NotImplementedError() raise NotImplementedError()
@classmethod @classmethod
@abstractmethod
def ohlcv_get_available_data( def ohlcv_get_available_data(
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes: cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
""" """
Returns a list of all pairs with ohlcv data available in this datadir Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files :param datadir: Directory to search for ohlcv files
:param trading_mode: trading-mode to be used :param trading_mode: trading-mode to be used
:return: List of Tuples of (pair, timeframe, CandleType) :return: List of Tuples of (pair, timeframe)
""" """
if trading_mode == TradingMode.FUTURES:
datadir = datadir.joinpath('futures')
_tmp = [
re.search(
cls._OHLCV_REGEX, p.name
) for p in datadir.glob(f"*.{cls._get_file_extension()}")]
return [
(
cls.rebuild_pair_from_filename(match[1]),
cls.rebuild_timeframe_from_filename(match[2]),
CandleType.from_string(match[3])
) for match in _tmp if match and len(match.groups()) > 1]
@classmethod @classmethod
@abstractmethod @abstractmethod

View File

@@ -8,9 +8,9 @@ from pandas import DataFrame, read_json, to_datetime
from freqtrade import misc from freqtrade import misc
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes, TradeList
from freqtrade.data.converter import trades_dict_to_list from freqtrade.data.converter import trades_dict_to_list
from freqtrade.enums import CandleType from freqtrade.enums import CandleType, TradingMode
from .idatahandler import IDataHandler from .idatahandler import IDataHandler
@@ -23,6 +23,28 @@ class JsonDataHandler(IDataHandler):
_use_zip = False _use_zip = False
_columns = DEFAULT_DATAFRAME_COLUMNS _columns = DEFAULT_DATAFRAME_COLUMNS
@classmethod
def ohlcv_get_available_data(
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
"""
Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files
:param trading_mode: trading-mode to be used
:return: List of Tuples of (pair, timeframe)
"""
if trading_mode == 'futures':
datadir = datadir.joinpath('futures')
_tmp = [
re.search(
cls._OHLCV_REGEX, p.name
) for p in datadir.glob(f"*.{cls._get_file_extension()}")]
return [
(
cls.rebuild_pair_from_filename(match[1]),
cls.rebuild_timeframe_from_filename(match[2]),
CandleType.from_string(match[3])
) for match in _tmp if match and len(match.groups()) > 1]
@classmethod @classmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]: def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
""" """

View File

@@ -9,12 +9,10 @@ class ExitType(Enum):
STOP_LOSS = "stop_loss" STOP_LOSS = "stop_loss"
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange" STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
TRAILING_STOP_LOSS = "trailing_stop_loss" TRAILING_STOP_LOSS = "trailing_stop_loss"
LIQUIDATION = "liquidation"
EXIT_SIGNAL = "exit_signal" EXIT_SIGNAL = "exit_signal"
FORCE_EXIT = "force_exit" FORCE_EXIT = "force_exit"
EMERGENCY_EXIT = "emergency_exit" EMERGENCY_EXIT = "emergency_exit"
CUSTOM_EXIT = "custom_exit" CUSTOM_EXIT = "custom_exit"
PARTIAL_EXIT = "partial_exit"
NONE = "" NONE = ""
def __str__(self): def __str__(self):

View File

@@ -17,8 +17,6 @@ class RPCMessageType(Enum):
PROTECTION_TRIGGER = 'protection_trigger' PROTECTION_TRIGGER = 'protection_trigger'
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global' PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
STRATEGY_MSG = 'strategy_msg'
def __repr__(self): def __repr__(self):
return self.value return self.value

View File

@@ -9,13 +9,12 @@ from freqtrade.exchange.bitpanda import Bitpanda
from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.bybit import Bybit
from freqtrade.exchange.coinbasepro import Coinbasepro from freqtrade.exchange.coinbasepro import Coinbasepro
from freqtrade.exchange.exchange import (amount_to_precision, available_exchanges, ccxt_exchanges, from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
date_minus_candles, is_exchange_known_ccxt, is_exchange_known_ccxt, is_exchange_officially_supported,
is_exchange_officially_supported, market_is_active, market_is_active, timeframe_to_minutes, timeframe_to_msecs,
price_to_precision, timeframe_to_minutes, timeframe_to_next_date, timeframe_to_prev_date,
timeframe_to_msecs, timeframe_to_next_date, timeframe_to_seconds, validate_exchange,
timeframe_to_prev_date, timeframe_to_seconds, validate_exchanges)
validate_exchange, validate_exchanges)
from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.ftx import Ftx
from freqtrade.exchange.gateio import Gateio from freqtrade.exchange.gateio import Gateio
from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.hitbtc import Hitbtc

View File

@@ -16,7 +16,7 @@ import arrow
import ccxt import ccxt
import ccxt.async_support as ccxt_async import ccxt.async_support as ccxt_async
from cachetools import TTLCache from cachetools import TTLCache
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, Precise, decimal_to_precision
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell, from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell,
@@ -32,7 +32,6 @@ from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGE
retrier_async) retrier_async)
from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2 from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.util import FtPrecise
CcxtModuleType = Any CcxtModuleType = Any
@@ -116,7 +115,6 @@ class Exchange:
self._last_markets_refresh: int = 0 self._last_markets_refresh: int = 0
# Cache for 10 minutes ... # Cache for 10 minutes ...
self._cache_lock = Lock()
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10) self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10)
# Cache values for 1800 to avoid frequent polling of the exchange for prices # Cache values for 1800 to avoid frequent polling of the exchange for prices
# Caching only applies to RPC methods, so prices for open trades are still # Caching only applies to RPC methods, so prices for open trades are still
@@ -681,35 +679,45 @@ class Exchange:
""" """
return endpoint in self._api.has and self._api.has[endpoint] return endpoint in self._api.has and self._api.has[endpoint]
def get_precision_amount(self, pair: str) -> Optional[float]:
"""
Returns the amount precision of the exchange.
:param pair: Pair to get precision for
:return: precision for amount or None. Must be used in combination with precisionMode
"""
return self.markets.get(pair, {}).get('precision', {}).get('amount', None)
def get_precision_price(self, pair: str) -> Optional[float]:
"""
Returns the price precision of the exchange.
:param pair: Pair to get precision for
:return: precision for price or None. Must be used in combination with precisionMode
"""
return self.markets.get(pair, {}).get('precision', {}).get('price', None)
def amount_to_precision(self, pair: str, amount: float) -> float: def amount_to_precision(self, pair: str, amount: float) -> float:
""" """
Returns the amount to buy or sell to a precision the Exchange accepts Returns the amount to buy or sell to a precision the Exchange accepts
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions.
""" """
return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode) if self.markets[pair]['precision']['amount'] is not None:
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
precision=self.markets[pair]['precision']['amount'],
counting_mode=self.precisionMode,
))
return amount
def price_to_precision(self, pair: str, price: float) -> float: def price_to_precision(self, pair: str, price: float) -> float:
""" """
Returns the price rounded up to the precision the Exchange accepts. Returns the price rounded up to the precision the Exchange accepts.
Partial Re-implementation of ccxt internal method decimal_to_precision(),
which does not support rounding up
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
align with amount_to_precision().
Rounds up Rounds up
""" """
return price_to_precision(price, self.get_precision_price(pair), self.precisionMode) if self.markets[pair]['precision']['price']:
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
# precision=self.markets[pair]['precision']['price'],
# counting_mode=self.precisionMode,
# ))
if self.precisionMode == TICK_SIZE:
precision = Precise(str(self.markets[pair]['precision']['price']))
price_str = Precise(str(price))
missing = price_str % precision
if not missing == Precise("0"):
price = round(float(str(price_str - missing + precision)), 14)
else:
symbol_prec = self.markets[pair]['precision']['price']
big_price = price * pow(10, symbol_prec)
price = ceil(big_price) / pow(10, symbol_prec)
return price
def price_get_one_pip(self, pair: str, price: float) -> float: def price_get_one_pip(self, pair: str, price: float) -> float:
""" """
@@ -841,7 +849,6 @@ class Exchange:
dry_order.update({ dry_order.update({
'average': average, 'average': average,
'filled': _amount, 'filled': _amount,
'remaining': 0.0,
'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
@@ -1010,8 +1017,7 @@ class Exchange:
time_in_force: str = 'gtc', time_in_force: str = 'gtc',
) -> Dict: ) -> Dict:
if self._config['dry_run']: if self._config['dry_run']:
dry_order = self.create_dry_run_order( dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage)
pair, ordertype, side, amount, self.price_to_precision(pair, rate), leverage)
return dry_order return dry_order
params = self._get_params(side, ordertype, leverage, reduceOnly, time_in_force) params = self._get_params(side, ordertype, leverage, reduceOnly, time_in_force)
@@ -1326,19 +1332,11 @@ class Exchange:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier @retrier
def fetch_positions(self, pair: str = None) -> List[Dict]: def fetch_positions(self) -> List[Dict]:
"""
Fetch positions from the exchange.
If no pair is given, all positions are returned.
:param pair: Pair for the query
"""
if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES: if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES:
return [] return []
try: try:
symbols = [] positions: List[Dict] = self._api.fetch_positions()
if pair:
symbols.append(pair)
positions: List[Dict] = self._api.fetch_positions(symbols)
self._log_exchange_response('fetch_positions', positions) self._log_exchange_response('fetch_positions', positions)
return positions return positions
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
@@ -1379,13 +1377,11 @@ class Exchange:
if not self.exchange_has('fetchBidsAsks'): if not self.exchange_has('fetchBidsAsks'):
return {} return {}
if cached: if cached:
with self._cache_lock:
tickers = self._fetch_tickers_cache.get('fetch_bids_asks') tickers = self._fetch_tickers_cache.get('fetch_bids_asks')
if tickers: if tickers:
return tickers return tickers
try: try:
tickers = self._api.fetch_bids_asks(symbols) tickers = self._api.fetch_bids_asks(symbols)
with self._cache_lock:
self._fetch_tickers_cache['fetch_bids_asks'] = tickers self._fetch_tickers_cache['fetch_bids_asks'] = tickers
return tickers return tickers
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
@@ -1407,13 +1403,11 @@ class Exchange:
:return: fetch_tickers result :return: fetch_tickers result
""" """
if cached: if cached:
with self._cache_lock:
tickers = self._fetch_tickers_cache.get('fetch_tickers') tickers = self._fetch_tickers_cache.get('fetch_tickers')
if tickers: if tickers:
return tickers return tickers
try: try:
tickers = self._api.fetch_tickers(symbols) tickers = self._api.fetch_tickers(symbols)
with self._cache_lock:
self._fetch_tickers_cache['fetch_tickers'] = tickers self._fetch_tickers_cache['fetch_tickers'] = tickers
return tickers return tickers
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
@@ -1505,8 +1499,7 @@ class Exchange:
return price_side return price_side
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) -> float:
order_book: Optional[dict] = None, ticker: Optional[dict] = 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
@@ -1523,7 +1516,6 @@ class Exchange:
cache_rate: TTLCache = self._entry_rate_cache if side == "entry" else self._exit_rate_cache cache_rate: TTLCache = self._entry_rate_cache if side == "entry" else self._exit_rate_cache
if not refresh: if not refresh:
with self._cache_lock:
rate = cache_rate.get(pair) rate = cache_rate.get(pair)
# Check if cache has been invalidated # Check if cache has been invalidated
if rate: if rate:
@@ -1539,7 +1531,6 @@ class Exchange:
if conf_strategy.get('use_order_book', False): if conf_strategy.get('use_order_book', False):
order_book_top = conf_strategy.get('order_book_top', 1) order_book_top = conf_strategy.get('order_book_top', 1)
if order_book is None:
order_book = self.fetch_l2_order_book(pair, order_book_top) order_book = self.fetch_l2_order_book(pair, order_book_top)
logger.debug('order_book %s', order_book) logger.debug('order_book %s', order_book)
# top 1 = index 0 # top 1 = index 0
@@ -1547,15 +1538,14 @@ class Exchange:
rate = order_book[f"{price_side}s"][order_book_top - 1][0] rate = order_book[f"{price_side}s"][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"{name} Price at location {order_book_top} from orderbook could not be "
f"could not be determined. Orderbook: {order_book}" f"determined. Orderbook: {order_book}"
) )
raise PricingError from e raise PricingError from e
logger.debug(f"{pair} - {name} price from orderbook {price_side_word}" logger.debug(f"{name} price from orderbook {price_side_word}"
f"side - top {order_book_top} order book {side} rate {rate:.8f}") f"side - top {order_book_top} order book {side} rate {rate:.8f}")
else: else:
logger.debug(f"Using Last {price_side_word} / Last Price") logger.debug(f"Using Last {price_side_word} / Last Price")
if ticker is None:
ticker = self.fetch_ticker(pair) ticker = self.fetch_ticker(pair)
ticker_rate = ticker[price_side] ticker_rate = ticker[price_side]
if ticker['last'] and ticker_rate: if ticker['last'] and ticker_rate:
@@ -1569,39 +1559,10 @@ class Exchange:
if rate is None: if rate is None:
raise PricingError(f"{name}-Rate for {pair} was empty.") raise PricingError(f"{name}-Rate for {pair} was empty.")
with self._cache_lock:
cache_rate[pair] = rate cache_rate[pair] = rate
return rate return rate
def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
entry_rate = None
exit_rate = None
if not refresh:
with self._cache_lock:
entry_rate = self._entry_rate_cache.get(pair)
exit_rate = self._exit_rate_cache.get(pair)
if entry_rate:
logger.debug(f"Using cached buy rate for {pair}.")
if exit_rate:
logger.debug(f"Using cached sell rate for {pair}.")
entry_pricing = self._config.get('entry_pricing', {})
exit_pricing = self._config.get('exit_pricing', {})
order_book = ticker = None
if not entry_rate and entry_pricing.get('use_order_book', False):
order_book_top = max(entry_pricing.get('order_book_top', 1),
exit_pricing.get('order_book_top', 1))
order_book = self.fetch_l2_order_book(pair, order_book_top)
entry_rate = self.get_rate(pair, refresh, 'entry', is_short, order_book=order_book)
elif not entry_rate:
ticker = self.fetch_ticker(pair)
entry_rate = self.get_rate(pair, refresh, 'entry', is_short, ticker=ticker)
if not exit_rate:
exit_rate = self.get_rate(pair, refresh, 'exit',
is_short, order_book=order_book, ticker=ticker)
return entry_rate, exit_rate
# Fee handling # Fee handling
@retrier @retrier
@@ -2245,14 +2206,10 @@ class Exchange:
coros = [self.get_market_leverage_tiers(symbol) for symbol in sorted(symbols)] coros = [self.get_market_leverage_tiers(symbol) for symbol in sorted(symbols)]
async def gather_results():
return await asyncio.gather(*input_coro, return_exceptions=True)
for input_coro in chunks(coros, 100): for input_coro in chunks(coros, 100):
with self._loop_lock: results = self.loop.run_until_complete(
results = self.loop.run_until_complete(gather_results()) asyncio.gather(*input_coro, return_exceptions=True))
for symbol, res in results: for symbol, res in results:
tiers[symbol] = res tiers[symbol] = res
@@ -2377,8 +2334,7 @@ class Exchange:
return return
try: try:
res = self._api.set_leverage(symbol=pair, leverage=leverage) self._api.set_leverage(symbol=pair, leverage=leverage)
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:
@@ -2406,6 +2362,7 @@ class Exchange:
if self.trading_mode in TradingMode.SPOT: if self.trading_mode in TradingMode.SPOT:
return None return None
elif ( elif (
self.margin_mode == MarginMode.ISOLATED and
self.trading_mode == TradingMode.FUTURES self.trading_mode == TradingMode.FUTURES
): ):
wallet_balance = (amount * open_rate) / leverage wallet_balance = (amount * open_rate) / leverage
@@ -2421,7 +2378,7 @@ class Exchange:
return isolated_liq return isolated_liq
else: else:
raise OperationalException( raise OperationalException(
"Freqtrade currently only supports futures for leverage trading.") "Freqtrade only supports isolated futures for leverage trading")
def funding_fee_cutoff(self, open_date: datetime): def funding_fee_cutoff(self, open_date: datetime):
""" """
@@ -2441,8 +2398,7 @@ class Exchange:
return return
try: try:
res = self._api.set_margin_mode(margin_mode.value, pair, params) self._api.set_margin_mode(margin_mode.value, pair, params)
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.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
@@ -2583,6 +2539,7 @@ class Exchange:
else: else:
return 0.0 return 0.0
@retrier
def get_or_calculate_liquidation_price( def get_or_calculate_liquidation_price(
self, self,
pair: str, pair: str,
@@ -2600,7 +2557,7 @@ class Exchange:
""" """
if self.trading_mode == TradingMode.SPOT: if self.trading_mode == TradingMode.SPOT:
return None return None
elif (self.trading_mode != TradingMode.FUTURES): elif (self.trading_mode != TradingMode.FUTURES and self.margin_mode != MarginMode.ISOLATED):
raise OperationalException( raise OperationalException(
f"{self.name} does not support {self.margin_mode.value} {self.trading_mode.value}") f"{self.name} does not support {self.margin_mode.value} {self.trading_mode.value}")
@@ -2616,12 +2573,20 @@ class Exchange:
upnl_ex_1=upnl_ex_1 upnl_ex_1=upnl_ex_1
) )
else: else:
positions = self.fetch_positions(pair) try:
positions = self._api.fetch_positions([pair])
if len(positions) > 0: if len(positions) > 0:
pos = positions[0] pos = positions[0]
isolated_liq = pos['liquidationPrice'] isolated_liq = pos['liquidationPrice']
else: else:
return None return None
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
if isolated_liq: if isolated_liq:
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
@@ -2853,61 +2818,3 @@ def market_is_active(market: Dict) -> bool:
# See https://github.com/ccxt/ccxt/issues/4874, # See https://github.com/ccxt/ccxt/issues/4874,
# https://github.com/ccxt/ccxt/issues/4075#issuecomment-434760520 # https://github.com/ccxt/ccxt/issues/4075#issuecomment-434760520
return market.get('active', True) is not False return market.get('active', True) is not False
def amount_to_precision(amount: float, amount_precision: Optional[float],
precisionMode: Optional[int]) -> float:
"""
Returns the amount to buy or sell to a precision the Exchange accepts
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions.
:param amount: amount to truncate
:param amount_precision: amount precision to use.
should be retrieved from markets[pair]['precision']['amount']
:param precisionMode: precision mode to use. Should be used from precisionMode
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
:return: truncated amount
"""
if amount_precision is not None and precisionMode is not None:
precision = int(amount_precision) if precisionMode != TICK_SIZE else amount_precision
# precision must be an int for non-ticksize inputs.
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
precision=precision,
counting_mode=precisionMode,
))
return amount
def price_to_precision(price: float, price_precision: Optional[float],
precisionMode: Optional[int]) -> float:
"""
Returns the price rounded up to the precision the Exchange accepts.
Partial Re-implementation of ccxt internal method decimal_to_precision(),
which does not support rounding up
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
align with amount_to_precision().
!!! Rounds up
:param price: price to convert
:param price_precision: price precision to use. Used from markets[pair]['precision']['price']
:param precisionMode: precision mode to use. Should be used from precisionMode
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
:return: price rounded up to the precision the Exchange accepts
"""
if price_precision is not None and precisionMode is not None:
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
# precision=price_precision,
# counting_mode=self.precisionMode,
# ))
if precisionMode == TICK_SIZE:
precision = FtPrecise(price_precision)
price_str = FtPrecise(price)
missing = price_str % precision
if not missing == FtPrecise("0"):
price = round(float(str(price_str - missing + precision)), 14)
else:
symbol_prec = price_precision
big_price = price * pow(10, symbol_prec)
price = ceil(big_price) / pow(10, symbol_prec)
return price

View File

@@ -1,6 +1,6 @@
""" FTX exchange subclass """ """ FTX exchange subclass """
import logging import logging
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Tuple
import ccxt import ccxt
@@ -116,17 +116,9 @@ class Ftx(Exchange):
if len(order) == 1: if len(order) == 1:
if order[0].get('status') == 'closed': if order[0].get('status') == 'closed':
# Trigger order was triggered ... # Trigger order was triggered ...
real_order_id: Optional[str] = order[0].get('info', {}).get('orderId') real_order_id = order[0].get('info', {}).get('orderId')
# OrderId may be None for stoploss-market orders # OrderId may be None for stoploss-market orders
# So we need to get it through the endpoint # But contains "average" in these cases.
# /conditional_orders/{conditional_order_id}/triggers
if not real_order_id:
res = self._api.privateGetConditionalOrdersConditionalOrderIdTriggers(
params={'conditional_order_id': order_id})
self._log_exchange_response('fetch_stoploss_order2', res)
real_order_id = res['result'][0]['orderId'] if res.get(
'result', []) else None
if real_order_id: if real_order_id:
order1 = self._api.fetch_order(real_order_id, pair) order1 = self._api.fetch_order(real_order_id, pair)
self._log_exchange_response('fetch_stoploss_order1', order1) self._log_exchange_response('fetch_stoploss_order1', order1)

View File

@@ -34,7 +34,6 @@ class Gateio(Exchange):
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"needs_trading_fees": True, "needs_trading_fees": True,
"ohlcv_volume_currency": "base",
"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'],
} }

View File

@@ -7,8 +7,9 @@ from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from freqtrade.enums.candletype import CandleType from freqtrade.enums.candletype import CandleType
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
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange import date_minus_candles
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -1,609 +0,0 @@
import collections
import json
import logging
import re
import shutil
import threading
from pathlib import Path
from typing import Any, Dict, Tuple, TypedDict
import numpy as np
import pandas as pd
import rapidjson
from joblib import dump, load
from joblib.externals import cloudpickle
from numpy.typing import NDArray
from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.data.history import load_pair_history
from freqtrade.exceptions import OperationalException
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from freqtrade.strategy.interface import IStrategy
logger = logging.getLogger(__name__)
class pair_info(TypedDict):
model_filename: str
first: bool
trained_timestamp: int
priority: int
data_path: str
extras: dict
class FreqaiDataDrawer:
"""
Class aimed at holding all pair models/info in memory for better inferencing/retrainig/saving
/loading to/from disk.
This object remains persistent throughout live/dry.
Record of contribution:
FreqAI was developed by a group of individuals who all contributed specific skillsets to the
project.
Conception and software development:
Robert Caulk @robcaulk
Theoretical brainstorming:
Elin Törnquist @th0rntwig
Code review, software architecture brainstorming:
@xmatthias
Beta testing and bug reporting:
@bloodhunter4rc, Salah Lamkadem @ikonx, @ken11o2, @longyu, @paranoidandy, @smidelis, @smarm
Juha Nykänen @suikula, Wagner Costa @wagnercosta, Johan Vlugt @Jooopieeert
"""
def __init__(self, full_path: Path, config: dict, follow_mode: bool = False):
self.config = config
self.freqai_info = config.get("freqai", {})
# dictionary holding all pair metadata necessary to load in from disk
self.pair_dict: Dict[str, pair_info] = {}
# dictionary holding all actively inferenced models in memory given a model filename
self.model_dictionary: Dict[str, Any] = {}
self.model_return_values: Dict[str, DataFrame] = {}
self.historic_data: Dict[str, Dict[str, DataFrame]] = {}
self.historic_predictions: Dict[str, DataFrame] = {}
self.follower_dict: Dict[str, pair_info] = {}
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.pair_dictionary_path = Path(self.full_path / "pair_dictionary.json")
self.follow_mode = follow_mode
if follow_mode:
self.create_follower_dict()
self.load_drawer_from_disk()
self.load_historic_predictions_from_disk()
self.training_queue: Dict[str, int] = {}
self.history_lock = threading.Lock()
self.save_lock = threading.Lock()
self.pair_dict_lock = threading.Lock()
self.old_DBSCAN_eps: Dict[str, float] = {}
self.empty_pair_dict: pair_info = {
"model_filename": "", "trained_timestamp": 0,
"priority": 1, "first": True, "data_path": "", "extras": {}}
def load_drawer_from_disk(self):
"""
Locate and load a previously saved data drawer full of all pair model metadata in
present model folder.
:return: bool - whether or not the drawer was located
"""
exists = self.pair_dictionary_path.is_file()
if exists:
with open(self.pair_dictionary_path, "r") as fp:
self.pair_dict = json.load(fp)
elif not self.follow_mode:
logger.info("Could not find existing datadrawer, starting from scratch")
else:
logger.warning(
f"Follower could not find pair_dictionary at {self.full_path} "
"sending null values back to strategy"
)
return exists
def load_historic_predictions_from_disk(self):
"""
Locate and load a previously saved historic predictions.
:return: bool - whether or not the drawer was located
"""
exists = self.historic_predictions_path.is_file()
if exists:
with open(self.historic_predictions_path, "rb") as fp:
self.historic_predictions = cloudpickle.load(fp)
logger.info(
f"Found existing historic predictions at {self.full_path}, but beware "
"that statistics may be inaccurate if the bot has been offline for "
"an extended period of time."
)
elif not self.follow_mode:
logger.info("Could not find existing historic_predictions, starting from scratch")
else:
logger.warning(
f"Follower could not find historic predictions at {self.full_path} "
"sending null values back to strategy"
)
return exists
def save_historic_predictions_to_disk(self):
"""
Save data drawer full of all pair model metadata in present model folder.
"""
with open(self.historic_predictions_path, "wb") as fp:
cloudpickle.dump(self.historic_predictions, fp, protocol=cloudpickle.DEFAULT_PROTOCOL)
def save_drawer_to_disk(self):
"""
Save data drawer full of all pair model metadata in present model folder.
"""
with self.save_lock:
with open(self.pair_dictionary_path, 'w') as fp:
rapidjson.dump(self.pair_dict, fp, default=self.np_encoder,
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 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):
if isinstance(object, np.generic):
return object.item()
def get_pair_dict_info(self, pair: str) -> Tuple[str, int, bool]:
"""
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
training
:param pair: str: pair to lookup
:return:
model_filename: str = unique filename used for loading persistent objects from disk
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)
data_path_set = self.pair_dict.get(pair, self.empty_pair_dict).get("data_path", "")
return_null_array = False
if pair_dict:
model_filename = pair_dict["model_filename"]
trained_timestamp = pair_dict["trained_timestamp"]
elif not self.follow_mode:
self.pair_dict[pair] = self.empty_pair_dict.copy()
model_filename = ""
trained_timestamp = 0
self.pair_dict[pair]["priority"] = len(self.pair_dict)
if not data_path_set and self.follow_mode:
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:
pair_in_dict = self.pair_dict.get(metadata["pair"])
if pair_in_dict:
return
else:
self.pair_dict[metadata["pair"]] = self.empty_pair_dict.copy()
self.pair_dict[metadata["pair"]]["priority"] = len(self.pair_dict)
return
def pair_to_end_of_training_queue(self, pair: str) -> None:
# march all pairs up in the queue
with self.pair_dict_lock:
for p in self.pair_dict:
self.pair_dict[p]["priority"] -= 1
# send pair to end of queue
self.pair_dict[pair]["priority"] = len(self.pair_dict)
def set_initial_return_values(self, pair: str, pred_df: DataFrame) -> None:
"""
Set the initial return values to the historical predictions dataframe. This avoids needing
to repredict on historical candles, and also stores historical predictions despite
retrainings (so stored predictions are true predictions, not just inferencing on trained
data)
"""
hist_df = self.historic_predictions
len_diff = len(hist_df[pair].index) - len(pred_df.index)
if len_diff < 0:
df_concat = pd.concat([pred_df.iloc[:abs(len_diff)], hist_df[pair]],
ignore_index=True, keys=hist_df[pair].keys())
else:
df_concat = hist_df[pair].tail(len(pred_df.index)).reset_index(drop=True)
df_concat = df_concat.fillna(0)
self.model_return_values[pair] = df_concat
def append_model_predictions(self, pair: str, predictions: DataFrame,
do_preds: NDArray[np.int_],
dk: FreqaiDataKitchen, len_df: int) -> None:
"""
Append model predictions to historic predictions dataframe, then set the
strategy return dataframe to the tail of the historic predictions. The length of
the tail is equivalent to the length of the dataframe that entered FreqAI from
the strategy originally. Doing this allows FreqUI to always display the correct
historic predictions.
"""
index = self.historic_predictions[pair].index[-1:]
columns = self.historic_predictions[pair].columns
nan_df = pd.DataFrame(np.nan, index=index, columns=columns)
self.historic_predictions[pair] = pd.concat(
[self.historic_predictions[pair], nan_df], ignore_index=True, axis=0)
df = self.historic_predictions[pair]
# model outputs and associated statistics
for label in predictions.columns:
df[label].iloc[-1] = predictions[label].iloc[-1]
if df[label].dtype == object:
continue
df[f"{label}_mean"].iloc[-1] = dk.data["labels_mean"][label]
df[f"{label}_std"].iloc[-1] = dk.data["labels_std"][label]
# outlier indicators
df["do_predict"].iloc[-1] = do_preds[-1]
if self.freqai_info["feature_parameters"].get("DI_threshold", 0) > 0:
df["DI_values"].iloc[-1] = dk.DI_values[-1]
# extra values the user added within custom prediction model
if dk.data['extra_returns_per_train']:
rets = dk.data['extra_returns_per_train']
for return_str in rets:
df[return_str].iloc[-1] = rets[return_str]
self.model_return_values[pair] = df.tail(len_df).reset_index(drop=True)
def attach_return_values_to_return_dataframe(
self, pair: str, dataframe: DataFrame) -> DataFrame:
"""
Attach the return values to the strat dataframe
:param dataframe: DataFrame = strategy dataframe
:return: DataFrame = strat dataframe with return values attached
"""
df = self.model_return_values[pair]
to_keep = [col for col in dataframe.columns if not col.startswith("&")]
dataframe = pd.concat([dataframe[to_keep], df], axis=1)
return dataframe
def return_null_values_to_strategy(self, dataframe: DataFrame, dk: FreqaiDataKitchen) -> None:
"""
Build 0 filled dataframe to return to strategy
"""
dk.find_features(dataframe)
full_labels = dk.label_list + dk.unique_class_list
for label in full_labels:
dataframe[label] = 0
dataframe[f"{label}_mean"] = 0
dataframe[f"{label}_std"] = 0
dataframe["do_predict"] = 0
if self.freqai_info["feature_parameters"].get("DI_threshold", 0) > 0:
dataframe["DI_values"] = 0
if dk.data['extra_returns_per_train']:
rets = dk.data['extra_returns_per_train']
for return_str in rets:
dataframe[return_str] = 0
dk.return_dataframe = dataframe
def purge_old_models(self) -> None:
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
pattern = re.compile(r"sub-train-(\w+)_(\d{10})")
delete_dict: Dict[str, Any] = {}
for dir in model_folders:
result = pattern.match(str(dir.name))
if result is None:
break
coin = result.group(1)
timestamp = result.group(2)
if coin not in delete_dict:
delete_dict[coin] = {}
delete_dict[coin]["num_folders"] = 1
delete_dict[coin]["timestamps"] = {int(timestamp): dir}
else:
delete_dict[coin]["num_folders"] += 1
delete_dict[coin]["timestamps"][int(timestamp)] = dir
for coin in delete_dict:
if delete_dict[coin]["num_folders"] > 2:
sorted_dict = collections.OrderedDict(
sorted(delete_dict[coin]["timestamps"].items())
)
num_delete = len(sorted_dict) - 2
deleted = 0
for k, v in sorted_dict.items():
if deleted >= num_delete:
break
logger.info(f"Freqai purging old model file {v}")
shutil.rmtree(v)
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()
# Functions pulled back from FreqaiDataKitchen because they relied on DataDrawer
def save_data(self, model: Any, coin: str, dk: FreqaiDataKitchen) -> None:
"""
Saves all data associated with a model for a single sub-train time range
:params:
:model: User trained model which can be reused for inferencing to generate
predictions
"""
if not dk.data_path.is_dir():
dk.data_path.mkdir(parents=True, exist_ok=True)
save_path = Path(dk.data_path)
# Save the trained model
if not dk.keras:
dump(model, save_path / f"{dk.model_filename}_model.joblib")
else:
model.save(save_path / f"{dk.model_filename}_model.h5")
if dk.svm_model is not None:
dump(dk.svm_model, save_path / f"{dk.model_filename}_svm_model.joblib")
dk.data["data_path"] = str(dk.data_path)
dk.data["model_filename"] = str(dk.model_filename)
dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns)
dk.data["label_list"] = dk.label_list
# store the metadata
with open(save_path / f"{dk.model_filename}_metadata.json", "w") as fp:
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE)
# save the train data to file so we can check preds for area of applicability later
dk.data_dictionary["train_features"].to_pickle(
save_path / f"{dk.model_filename}_trained_df.pkl"
)
dk.data_dictionary["train_dates"].to_pickle(
save_path / f"{dk.model_filename}_trained_dates_df.pkl"
)
if self.freqai_info["feature_parameters"].get("principal_component_analysis"):
cloudpickle.dump(
dk.pca, open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "wb")
)
# if self.live:
self.model_dictionary[dk.model_filename] = model
self.pair_dict[coin]["model_filename"] = dk.model_filename
self.pair_dict[coin]["data_path"] = str(dk.data_path)
self.save_drawer_to_disk()
return
def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any:
"""
loads all data required to make a prediction on a sub-train time range
:returns:
:model: User trained model which can be inferenced for new predictions
"""
if not self.pair_dict[coin]["model_filename"]:
return None
if dk.live:
dk.model_filename = self.pair_dict[coin]["model_filename"]
dk.data_path = Path(self.pair_dict[coin]["data_path"])
if self.freqai_info.get("follow_mode", False):
# follower can be on a different system which is rsynced from the leader:
dk.data_path = Path(
self.config["user_data_dir"]
/ "models"
/ dk.data_path.parts[-2]
/ dk.data_path.parts[-1]
)
with open(dk.data_path / f"{dk.model_filename}_metadata.json", "r") as fp:
dk.data = json.load(fp)
dk.training_features_list = dk.data["training_features_list"]
dk.label_list = dk.data["label_list"]
dk.data_dictionary["train_features"] = pd.read_pickle(
dk.data_path / f"{dk.model_filename}_trained_df.pkl"
)
# try to access model in memory instead of loading object from disk to save time
if dk.live and dk.model_filename in self.model_dictionary:
model = self.model_dictionary[dk.model_filename]
elif not dk.keras:
model = load(dk.data_path / f"{dk.model_filename}_model.joblib")
else:
from tensorflow import keras
model = keras.models.load_model(dk.data_path / f"{dk.model_filename}_model.h5")
if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file():
dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib")
if not model:
raise OperationalException(
f"Unable to load model, ensure model exists at " f"{dk.data_path} "
)
if self.config["freqai"]["feature_parameters"]["principal_component_analysis"]:
dk.pca = cloudpickle.load(
open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "rb")
)
return model
def update_historic_data(self, strategy: IStrategy, dk: FreqaiDataKitchen) -> None:
"""
Append new candles to our stores historic data (in memory) so that
we do not need to load candle history from disk and we dont need to
pinging exchange multiple times for the same candle.
:params:
dataframe: DataFrame = strategy provided dataframe
"""
feat_params = self.freqai_info["feature_parameters"]
with self.history_lock:
history_data = self.historic_data
for pair in dk.all_pairs:
for tf in feat_params.get("include_timeframes"):
# check if newest candle is already appended
df_dp = strategy.dp.get_pair_dataframe(pair, tf)
if len(df_dp.index) == 0:
continue
if str(history_data[pair][tf].iloc[-1]["date"]) == str(
df_dp.iloc[-1:]["date"].iloc[-1]
):
continue
try:
index = (
df_dp.loc[
df_dp["date"] == history_data[pair][tf].iloc[-1]["date"]
].index[0]
+ 1
)
except IndexError:
logger.warning(
f"Unable to update pair history for {pair}. "
"If this does not resolve itself after 1 additional candle, "
"please report the error to #freqai discord channel"
)
return
history_data[pair][tf] = pd.concat(
[
history_data[pair][tf],
df_dp.iloc[index:],
],
ignore_index=True,
axis=0,
)
def load_all_pair_histories(self, timerange: TimeRange, dk: FreqaiDataKitchen) -> None:
"""
Load pair histories for all whitelist and corr_pairlist pairs.
Only called once upon startup of bot.
:params:
timerange: TimeRange = full timerange required to populate all indicators
for training according to user defined train_period_days
"""
history_data = self.historic_data
for pair in dk.all_pairs:
if pair not in history_data:
history_data[pair] = {}
for tf in self.freqai_info["feature_parameters"].get("include_timeframes"):
history_data[pair][tf] = load_pair_history(
datadir=self.config["datadir"],
timeframe=tf,
pair=pair,
timerange=timerange,
data_format=self.config.get("dataformat_ohlcv", "json"),
candle_type=self.config.get("trading_mode", "spot"),
)
def get_base_and_corr_dataframes(
self, timerange: TimeRange, pair: str, dk: FreqaiDataKitchen
) -> Tuple[Dict[Any, Any], Dict[Any, Any]]:
"""
Searches through our historic_data in memory and returns the dataframes relevant
to the present pair.
:params:
timerange: TimeRange = full timerange required to populate all indicators
for training according to user defined train_period_days
metadata: dict = strategy furnished pair metadata
"""
with self.history_lock:
corr_dataframes: Dict[Any, Any] = {}
base_dataframes: Dict[Any, Any] = {}
historic_data = self.historic_data
pairs = self.freqai_info["feature_parameters"].get(
"include_corr_pairlist", []
)
for tf in self.freqai_info["feature_parameters"].get("include_timeframes"):
base_dataframes[tf] = dk.slice_dataframe(timerange, historic_data[pair][tf])
if pairs:
for p in pairs:
if pair in p:
continue # dont repeat anything from whitelist
if p not in corr_dataframes:
corr_dataframes[p] = {}
corr_dataframes[p][tf] = dk.slice_dataframe(
timerange, historic_data[p][tf]
)
return corr_dataframes, base_dataframes
# to be used if we want to send predictions directly to the follower instead of forcing
# follower to load models and inference
# def save_model_return_values_to_disk(self) -> None:
# with open(self.full_path / str('model_return_values.json'), "w") as fp:
# json.dump(self.model_return_values, fp, default=self.np_encoder)
# def load_model_return_values_from_disk(self, dk: FreqaiDataKitchen) -> FreqaiDataKitchen:
# exists = Path(self.full_path / str('model_return_values.json')).resolve().exists()
# if exists:
# with open(self.full_path / str('model_return_values.json'), "r") as fp:
# self.model_return_values = json.load(fp)
# elif not self.follow_mode:
# logger.info("Could not find existing datadrawer, starting from scratch")
# else:
# logger.warning(f'Follower could not find pair_dictionary at {self.full_path} '
# 'sending null values back to strategy')
# return exists, dk

File diff suppressed because it is too large Load Diff

View File

@@ -1,663 +0,0 @@
# import contextlib
import datetime
import logging
import shutil
import threading
import time
from abc import ABC, abstractmethod
from pathlib import Path
from threading import Lock
from typing import Any, Dict, Tuple
import numpy as np
import pandas as pd
from numpy.typing import NDArray
from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_seconds
from freqtrade.freqai.data_drawer import FreqaiDataDrawer
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from freqtrade.strategy.interface import IStrategy
pd.options.mode.chained_assignment = None
logger = logging.getLogger(__name__)
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
return wrapper
class IFreqaiModel(ABC):
"""
Class containing all tools for training and prediction in the strategy.
Base*PredictionModels inherit from this class.
Record of contribution:
FreqAI was developed by a group of individuals who all contributed specific skillsets to the
project.
Conception and software development:
Robert Caulk @robcaulk
Theoretical brainstorming:
Elin Törnquist @th0rntwig
Code review, software architecture brainstorming:
@xmatthias
Beta testing and bug reporting:
@bloodhunter4rc, Salah Lamkadem @ikonx, @ken11o2, @longyu, @paranoidandy, @smidelis, @smarm
Juha Nykänen @suikula, Wagner Costa @wagnercosta, Johan Vlugt @Jooopieeert
"""
def __init__(self, config: Dict[str, Any]) -> None:
self.config = config
self.assert_config(self.config)
self.freqai_info: Dict[str, Any] = config["freqai"]
self.data_split_parameters: Dict[str, Any] = config.get("freqai", {}).get(
"data_split_parameters", {})
self.model_training_parameters: Dict[str, Any] = config.get("freqai", {}).get(
"model_training_parameters", {})
self.retrain = False
self.first = True
self.set_full_path()
self.follow_mode: bool = self.freqai_info.get("follow_mode", False)
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode)
self.identifier: str = self.freqai_info.get("identifier", "no_id_provided")
self.scanning = False
self.ft_params = self.freqai_info["feature_parameters"]
self.keras: bool = self.freqai_info.get("keras", False)
if self.keras and self.ft_params.get("DI_threshold", 0):
self.ft_params["DI_threshold"] = 0
logger.warning("DI threshold is not configured for Keras models yet. Deactivating.")
self.CONV_WIDTH = self.freqai_info.get("conv_width", 2)
if self.ft_params.get("inlier_metric_window", 0):
self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2
self.pair_it = 0
self.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist"))
self.last_trade_database_summary: DataFrame = {}
self.current_trade_database_summary: DataFrame = {}
self.analysis_lock = Lock()
self.inference_time: float = 0
self.begin_time: float = 0
self.base_tf_seconds = timeframe_to_seconds(self.config['timeframe'])
def assert_config(self, config: Dict[str, Any]) -> None:
if not config.get("freqai", {}):
raise OperationalException("No freqai parameters found in configuration file.")
def start(self, dataframe: DataFrame, metadata: dict, strategy: IStrategy) -> DataFrame:
"""
Entry point to the FreqaiModel from a specific pair, it will train a new model if
necessary before making the prediction.
:param dataframe: Full dataframe coming from strategy - it contains entire
backtesting timerange + additional historical data necessary to train
the model.
:param metadata: pair metadata coming from strategy.
:param strategy: Strategy to train on
"""
self.live = strategy.dp.runmode in (RunMode.DRY_RUN, RunMode.LIVE)
self.dd.set_pair_dict_info(metadata)
if self.live:
self.inference_timer('start')
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
dk = self.start_live(dataframe, metadata, strategy, self.dk)
# For backtesting, each pair enters and then gets trained for each window along the
# sliding window defined by "train_period_days" (training window) and "live_retrain_hours"
# (backtest window, i.e. window immediately following the training window).
# FreqAI slides the window and sequentially builds the backtesting results before returning
# the concatenated results for the full backtesting period back to the strategy.
elif not self.follow_mode:
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
logger.info(f"Training {len(self.dk.training_timeranges)} timeranges")
with self.analysis_lock:
dataframe = self.dk.use_strategy_to_populate_indicators(
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
)
dk = self.start_backtesting(dataframe, metadata, self.dk)
dataframe = dk.remove_features_from_df(dk.return_dataframe)
del dk
if self.live:
self.inference_timer('stop')
return dataframe
@threaded
def start_scanning(self, strategy: IStrategy) -> None:
"""
Function designed to constantly scan pairs for retraining on a separate thread (intracandle)
to improve model youth. This function is agnostic to data preparation/collection/storage,
it simply trains on what ever data is available in the self.dd.
:param strategy: IStrategy = The user defined strategy class
"""
while 1:
time.sleep(1)
for pair in self.config.get("exchange", {}).get("pair_whitelist"):
(_, trained_timestamp, _) = self.dd.get_pair_dict_info(pair)
if self.dd.pair_dict[pair]["priority"] != 1:
continue
dk = FreqaiDataKitchen(self.config, self.live, pair)
dk.set_paths(pair, trained_timestamp)
(
retrain,
new_trained_timerange,
data_load_timerange,
) = dk.check_if_new_training_required(trained_timestamp)
dk.set_paths(pair, new_trained_timerange.stopts)
if retrain:
self.train_model_in_series(
new_trained_timerange, pair, strategy, dk, data_load_timerange
)
self.dd.save_historic_predictions_to_disk()
def start_backtesting(
self, dataframe: DataFrame, metadata: dict, dk: FreqaiDataKitchen
) -> FreqaiDataKitchen:
"""
The main broad execution for backtesting. For backtesting, each pair enters and then gets
trained for each window along the sliding window defined by "train_period_days"
(training window) and "backtest_period_days" (backtest window, i.e. window immediately
following the training window). FreqAI slides the window and sequentially builds
the backtesting results before returning the concatenated results for the full
backtesting period back to the strategy.
:param dataframe: DataFrame = strategy passed dataframe
:param metadata: Dict = pair metadata
:param dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only
:return:
FreqaiDataKitchen = Data management/analysis tool associated to present pair only
"""
self.pair_it += 1
train_it = 0
# Loop enforcing the sliding window training/backtesting paradigm
# tr_train is the training time range e.g. 1 historical month
# tr_backtest is the backtesting time range e.g. the week directly
# following tr_train. Both of these windows slide through the
# entire backtest
for tr_train, tr_backtest in zip(dk.training_timeranges, dk.backtesting_timeranges):
(_, _, _) = self.dd.get_pair_dict_info(metadata["pair"])
train_it += 1
total_trains = len(dk.backtesting_timeranges)
self.training_timerange = tr_train
dataframe_train = dk.slice_dataframe(tr_train, dataframe)
dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe)
trained_timestamp = tr_train
tr_train_startts_str = datetime.datetime.utcfromtimestamp(tr_train.startts).strftime(
"%Y-%m-%d %H:%M:%S"
)
tr_train_stopts_str = datetime.datetime.utcfromtimestamp(tr_train.stopts).strftime(
"%Y-%m-%d %H:%M:%S"
)
logger.info(
f"Training {metadata['pair']}, {self.pair_it}/{self.total_pairs} pairs"
f" from {tr_train_startts_str} to {tr_train_stopts_str}, {train_it}/{total_trains} "
"trains"
)
dk.data_path = Path(
dk.full_path
/
f"sub-train-{metadata['pair'].split('/')[0]}_{int(trained_timestamp.stopts)}"
)
if not self.model_exists(
metadata["pair"], dk, trained_timestamp=int(trained_timestamp.stopts)
):
dk.find_features(dataframe_train)
self.model = self.train(dataframe_train, metadata["pair"], dk)
self.dd.pair_dict[metadata["pair"]]["trained_timestamp"] = int(
trained_timestamp.stopts)
dk.set_new_model_names(metadata["pair"], trained_timestamp)
self.dd.save_data(self.model, metadata["pair"], dk)
else:
self.model = self.dd.load_data(metadata["pair"], dk)
self.check_if_feature_list_matches_strategy(dataframe_train, dk)
pred_df, do_preds = self.predict(dataframe_backtest, dk)
dk.append_predictions(pred_df, do_preds)
dk.fill_predictions(dataframe)
return dk
def start_live(
self, dataframe: DataFrame, metadata: dict, strategy: IStrategy, dk: FreqaiDataKitchen
) -> FreqaiDataKitchen:
"""
The main broad execution for dry/live. This function will check if a retraining should be
performed, and if so, retrain and reset the model.
:param dataframe: DataFrame = strategy passed dataframe
:param metadata: Dict = pair metadata
:param strategy: IStrategy = currently employed strategy
dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only
:returns:
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
(_, trained_timestamp, return_null_array) = 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
if self.dd.historic_data:
self.dd.update_historic_data(strategy, dk)
logger.debug(f'Updating historic data on pair {metadata["pair"]}')
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)
# download candle history if it is not already in memory
if not self.dd.historic_data:
logger.info(
"Downloading all training data for all pairs in whitelist and "
"corr_pairlist, this may take a while if you do not have the "
"data saved"
)
dk.download_all_data_for_training(data_load_timerange, strategy.dp)
self.dd.load_all_pair_histories(data_load_timerange, dk)
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
self.model = self.dd.load_data(metadata["pair"], dk)
with self.analysis_lock:
dataframe = self.dk.use_strategy_to_populate_indicators(
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
)
if not self.model:
logger.warning(
f"No model ready for {metadata['pair']}, returning null values to strategy."
)
self.dd.return_null_values_to_strategy(dataframe, dk)
return dk
# ensure user is feeding the correct indicators to the model
self.check_if_feature_list_matches_strategy(dataframe, dk)
self.build_strategy_return_arrays(dataframe, dk, metadata["pair"], trained_timestamp)
return dk
def build_strategy_return_arrays(
self, dataframe: DataFrame, dk: FreqaiDataKitchen, pair: str, trained_timestamp: int
) -> None:
# hold the historical predictions in memory so we are sending back
# correct array to strategy
if pair not in self.dd.model_return_values:
# first predictions are made on entire historical candle set coming from strategy. This
# allows FreqUI to show full return values.
pred_df, do_preds = self.predict(dataframe, dk)
if pair not in self.dd.historic_predictions:
self.set_initial_historic_predictions(pred_df, dk, pair)
self.dd.set_initial_return_values(pair, pred_df)
dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe)
return
elif self.dk.check_if_model_expired(trained_timestamp):
pred_df = DataFrame(np.zeros((2, len(dk.label_list))), columns=dk.label_list)
do_preds = np.ones(2, dtype=np.int_) * 2
dk.DI_values = np.zeros(2)
logger.warning(
f"Model expired for {pair}, returning null values to strategy. Strategy "
"construction should take care to consider this event with "
"prediction == 0 and do_predict == 2"
)
else:
# remaining predictions are made only on the most recent candles for performance and
# historical accuracy reasons.
pred_df, do_preds = self.predict(dataframe.iloc[-self.CONV_WIDTH:], dk, first=False)
if self.freqai_info.get('fit_live_predictions_candles', 0) and self.live:
self.fit_live_predictions(dk, pair)
self.dd.append_model_predictions(pair, pred_df, do_preds, dk, len(dataframe))
dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe)
return
def check_if_feature_list_matches_strategy(
self, dataframe: DataFrame, dk: FreqaiDataKitchen
) -> None:
"""
Ensure user is passing the proper feature set if they are reusing an `identifier` pointing
to a folder holding existing models.
:param dataframe: DataFrame = strategy provided dataframe
:param dk: FreqaiDataKitchen = non-persistent data container/analyzer for
current coin/bot loop
"""
dk.find_features(dataframe)
if "training_features_list_raw" in dk.data:
feature_list = dk.data["training_features_list_raw"]
else:
feature_list = dk.training_features_list
if dk.training_features_list != feature_list:
raise OperationalException(
"Trying to access pretrained model with `identifier` "
"but found different features furnished by current strategy."
"Change `identifier` to train from scratch, or ensure the"
"strategy is furnishing the same features as the pretrained"
"model"
)
def data_cleaning_train(self, dk: FreqaiDataKitchen) -> None:
"""
Base data cleaning method for train.
Functions here improve/modify the input data by identifying outliers,
computing additional metrics, adding noise, reducing dimensionality etc.
"""
ft_params = self.freqai_info["feature_parameters"]
if ft_params.get(
"principal_component_analysis", False
):
dk.principal_component_analysis()
if ft_params.get("use_SVM_to_remove_outliers", False):
dk.use_SVM_to_remove_outliers(predict=False)
if ft_params.get("DI_threshold", 0):
dk.data["avg_mean_dist"] = dk.compute_distances()
if ft_params.get("use_DBSCAN_to_remove_outliers", False):
if dk.pair in self.dd.old_DBSCAN_eps:
eps = self.dd.old_DBSCAN_eps[dk.pair]
else:
eps = None
dk.use_DBSCAN_to_remove_outliers(predict=False, eps=eps)
self.dd.old_DBSCAN_eps[dk.pair] = dk.data['DBSCAN_eps']
if ft_params.get('inlier_metric_window', 0):
dk.compute_inlier_metric(set_='train')
if self.freqai_info["data_split_parameters"]["test_size"] > 0:
dk.compute_inlier_metric(set_='test')
if self.freqai_info["feature_parameters"].get('noise_standard_deviation', 0):
dk.add_noise_to_training_features()
def data_cleaning_predict(self, dk: FreqaiDataKitchen, dataframe: DataFrame) -> None:
"""
Base data cleaning method for predict.
Functions here are complementary to the functions of data_cleaning_train.
"""
ft_params = self.freqai_info["feature_parameters"]
if ft_params.get('inlier_metric_window', 0):
dk.compute_inlier_metric(set_='predict')
if ft_params.get(
"principal_component_analysis", False
):
dk.pca_transform(dataframe)
if ft_params.get("use_SVM_to_remove_outliers", False):
dk.use_SVM_to_remove_outliers(predict=True)
if ft_params.get("DI_threshold", 0):
dk.check_if_pred_in_training_spaces()
if ft_params.get("use_DBSCAN_to_remove_outliers", False):
dk.use_DBSCAN_to_remove_outliers(predict=True)
def model_exists(
self,
pair: str,
dk: FreqaiDataKitchen,
trained_timestamp: int = None,
model_filename: str = "",
scanning: bool = False,
) -> bool:
"""
Given a pair and path, check if a model already exists
:param pair: pair e.g. BTC/USD
:param path: path to model
:return:
:boolean: whether the model file exists or not.
"""
coin, _ = pair.split("/")
if not self.live:
dk.model_filename = model_filename = f"cb_{coin.lower()}_{trained_timestamp}"
path_to_modelfile = Path(dk.data_path / f"{model_filename}_model.joblib")
file_exists = path_to_modelfile.is_file()
if file_exists and not scanning:
logger.info("Found model at %s", dk.data_path / dk.model_filename)
elif not scanning:
logger.info("Could not find model at %s", dk.data_path / dk.model_filename)
return file_exists
def set_full_path(self) -> None:
self.full_path = Path(
self.config["user_data_dir"] / "models" / f"{self.freqai_info['identifier']}"
)
self.full_path.mkdir(parents=True, exist_ok=True)
shutil.copy(
self.config["config_files"][0],
Path(self.full_path, Path(self.config["config_files"][0]).name),
)
def train_model_in_series(
self,
new_trained_timerange: TimeRange,
pair: str,
strategy: IStrategy,
dk: FreqaiDataKitchen,
data_load_timerange: TimeRange,
):
"""
Retrieve data and train model in single threaded mode (only used if model directory is empty
upon startup for dry/live )
:param new_trained_timerange: TimeRange = the timerange to train the model on
:param metadata: dict = strategy provided metadata
:param strategy: IStrategy = user defined strategy object
:param dk: FreqaiDataKitchen = non-persistent data container for current coin/loop
:param data_load_timerange: TimeRange = the amount of data to be loaded
for populate_any_indicators
(larger than new_trained_timerange so that
new_trained_timerange does not contain any NaNs)
"""
corr_dataframes, base_dataframes = self.dd.get_base_and_corr_dataframes(
data_load_timerange, pair, dk
)
with self.analysis_lock:
unfiltered_dataframe = dk.use_strategy_to_populate_indicators(
strategy, corr_dataframes, base_dataframes, pair
)
unfiltered_dataframe = dk.slice_dataframe(new_trained_timerange, unfiltered_dataframe)
# find the features indicated by strategy and store in datakitchen
dk.find_features(unfiltered_dataframe)
model = self.train(unfiltered_dataframe, pair, dk)
self.dd.pair_dict[pair]["trained_timestamp"] = new_trained_timerange.stopts
dk.set_new_model_names(pair, new_trained_timerange)
self.dd.pair_dict[pair]["first"] = False
if self.dd.pair_dict[pair]["priority"] == 1 and self.scanning:
self.dd.pair_to_end_of_training_queue(pair)
self.dd.save_data(model, pair, dk)
if self.freqai_info.get("purge_old_models", False):
self.dd.purge_old_models()
def set_initial_historic_predictions(
self, pred_df: DataFrame, dk: FreqaiDataKitchen, pair: str
) -> None:
"""
This function is called only if the datadrawer failed to load an
existing set of historic predictions. In this case, it builds
the structure and sets fake predictions off the first training
data. After that, FreqAI will append new real predictions to the
set of historic predictions.
These values are used to generate live statistics which can be used
in the strategy for adaptive values. E.g. &*_mean/std are quantities
that can computed based on live predictions from the set of historical
predictions. Those values can be used in the user strategy to better
assess prediction rarity, and thus wait for probabilistically favorable
entries relative to the live historical predictions.
If the user reuses an identifier on a subsequent instance,
this function will not be called. In that case, "real" predictions
will be appended to the loaded set of historic predictions.
:param: df: DataFrame = the dataframe containing the training feature data
:param: model: Any = A model which was `fit` using a common library such as
catboost or lightgbm
:param: dk: FreqaiDataKitchen = object containing methods for data analysis
:param: pair: str = current pair
"""
self.dd.historic_predictions[pair] = pred_df
hist_preds_df = self.dd.historic_predictions[pair]
for label in hist_preds_df.columns:
if hist_preds_df[label].dtype == object:
continue
hist_preds_df[f'{label}_mean'] = 0
hist_preds_df[f'{label}_std'] = 0
hist_preds_df['do_predict'] = 0
if self.freqai_info['feature_parameters'].get('DI_threshold', 0) > 0:
hist_preds_df['DI_values'] = 0
for return_str in dk.data['extra_returns_per_train']:
hist_preds_df[return_str] = 0
# # for keras type models, the conv_window needs to be prepended so
# # viewing is correct in frequi
if self.freqai_info.get('keras', False):
n_lost_points = self.freqai_info.get('conv_width', 2)
zeros_df = DataFrame(np.zeros((n_lost_points, len(hist_preds_df.columns))),
columns=hist_preds_df.columns)
self.dd.historic_predictions[pair] = pd.concat(
[zeros_df, hist_preds_df], axis=0, ignore_index=True)
def fit_live_predictions(self, dk: FreqaiDataKitchen, pair: str) -> None:
"""
Fit the labels with a gaussian distribution
"""
import scipy as spy
# add classes from classifier label types if used
full_labels = dk.label_list + dk.unique_class_list
num_candles = self.freqai_info.get("fit_live_predictions_candles", 100)
dk.data["labels_mean"], dk.data["labels_std"] = {}, {}
for label in full_labels:
if self.dd.historic_predictions[dk.pair][label].dtype == object:
continue
f = spy.stats.norm.fit(self.dd.historic_predictions[dk.pair][label].tail(num_candles))
dk.data["labels_mean"][label], dk.data["labels_std"][label] = f[0], f[1]
return
def inference_timer(self, do='start'):
"""
Timer designed to track the cumulative time spent in FreqAI for one pass through
the whitelist. This will check if the time spent is more than 1/4 the time
of a single candle, and if so, it will warn the user of degraded performance
"""
if do == 'start':
self.pair_it += 1
self.begin_time = time.time()
elif do == 'stop':
end = time.time()
self.inference_time += (end - self.begin_time)
if self.pair_it == self.total_pairs:
logger.info(
f'Total time spent inferencing pairlist {self.inference_time:.2f} seconds')
if self.inference_time > 0.25 * self.base_tf_seconds:
logger.warning('Inference took over 25/% of the candle time. Reduce pairlist to'
' avoid blinding open trades and degrading performance.')
self.pair_it = 0
self.inference_time = 0
return
# Following methods which are overridden by user made prediction models.
# See freqai/prediction_models/CatboostPredictionModel.py for an example.
@abstractmethod
def train(self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen) -> Any:
"""
Filter the training data and train a model to it. Train makes heavy use of the datahandler
for storing, saving, loading, and analyzing the data.
:param unfiltered_dataframe: Full dataframe for the current training period
:param metadata: pair metadata from strategy.
:return: Trained model which can be used to inference (self.predict)
"""
@abstractmethod
def fit(self, data_dictionary: Dict[str, Any]) -> Any:
"""
Most regressors use the same function names and arguments e.g. user
can drop in LGBMRegressor in place of CatBoostRegressor and all data
management will be properly handled by Freqai.
:param data_dictionary: Dict = the dictionary constructed by DataHandler to hold
all the training and test data/labels.
"""
return
@abstractmethod
def predict(
self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = True
) -> Tuple[DataFrame, NDArray[np.int_]]:
"""
Filter the prediction features data and predict with it.
:param unfiltered_dataframe: Full dataframe for the current backtest period.
:param dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only
:param first: boolean = whether this is the first prediction or not.
:return:
:predictions: np.array of predictions
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
data (NaNs) or felt uncertain about data (i.e. SVM and/or DI index)
"""

View File

@@ -1,99 +0,0 @@
import logging
from typing import Any, Tuple
import numpy as np
import numpy.typing as npt
import pandas as pd
from pandas import DataFrame
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from freqtrade.freqai.freqai_interface import IFreqaiModel
logger = logging.getLogger(__name__)
class BaseClassifierModel(IFreqaiModel):
"""
Base class for regression type models (e.g. Catboost, LightGBM, XGboost etc.).
User *must* inherit from this class and set fit() and predict(). See example scripts
such as prediction_models/CatboostPredictionModel.py for guidance.
"""
def train(
self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen
) -> Any:
"""
Filter the training data and train a model to it. Train makes heavy use of the datakitchen
for storing, saving, loading, and analyzing the data.
:param unfiltered_dataframe: Full dataframe for the current training period
:param metadata: pair metadata from strategy.
:return:
:model: Trained model which can be used to inference (self.predict)
"""
logger.info("-------------------- Starting training " f"{pair} --------------------")
# filter the features requested by user in the configuration file and elegantly handle NaNs
features_filtered, labels_filtered = dk.filter_features(
unfiltered_dataframe,
dk.training_features_list,
dk.label_list,
training_filter=True,
)
start_date = unfiltered_dataframe["date"].iloc[0].strftime("%Y-%m-%d")
end_date = unfiltered_dataframe["date"].iloc[-1].strftime("%Y-%m-%d")
logger.info(f"-------------------- Training on data from {start_date} to "
f"{end_date}--------------------")
# split data into train/test data.
data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered)
if not self.freqai_info.get('fit_live_predictions', 0) or not self.live:
dk.fit_labels()
# normalize all data based on train_dataset only
data_dictionary = dk.normalize_data(data_dictionary)
# optional additional data cleaning/analysis
self.data_cleaning_train(dk)
logger.info(
f'Training model on {len(dk.data_dictionary["train_features"].columns)}' " features"
)
logger.info(f'Training model on {len(data_dictionary["train_features"])} data points')
model = self.fit(data_dictionary)
logger.info(f"--------------------done training {pair}--------------------")
return model
def predict(
self, unfiltered_dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False
) -> Tuple[DataFrame, npt.NDArray[np.int_]]:
"""
Filter the prediction features data and predict with it.
:param: unfiltered_dataframe: Full dataframe for the current backtest period.
:return:
:pred_df: dataframe containing the predictions
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
data (NaNs) or felt uncertain about data (PCA and DI index)
"""
dk.find_features(unfiltered_dataframe)
filtered_dataframe, _ = dk.filter_features(
unfiltered_dataframe, dk.training_features_list, training_filter=False
)
filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe)
dk.data_dictionary["prediction_features"] = filtered_dataframe
self.data_cleaning_predict(dk, filtered_dataframe)
predictions = self.model.predict(dk.data_dictionary["prediction_features"])
pred_df = DataFrame(predictions, columns=dk.label_list)
predictions_prob = self.model.predict_proba(dk.data_dictionary["prediction_features"])
pred_df_prob = DataFrame(predictions_prob, columns=self.model.classes_)
pred_df = pd.concat([pred_df, pred_df_prob], axis=1)
return (pred_df, dk.do_predict)

View File

@@ -1,96 +0,0 @@
import logging
from typing import Any, Tuple
import numpy as np
import numpy.typing as npt
from pandas import DataFrame
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from freqtrade.freqai.freqai_interface import IFreqaiModel
logger = logging.getLogger(__name__)
class BaseRegressionModel(IFreqaiModel):
"""
Base class for regression type models (e.g. Catboost, LightGBM, XGboost etc.).
User *must* inherit from this class and set fit() and predict(). See example scripts
such as prediction_models/CatboostPredictionModel.py for guidance.
"""
def train(
self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen
) -> Any:
"""
Filter the training data and train a model to it. Train makes heavy use of the datakitchen
for storing, saving, loading, and analyzing the data.
:param unfiltered_dataframe: Full dataframe for the current training period
:param metadata: pair metadata from strategy.
:return:
:model: Trained model which can be used to inference (self.predict)
"""
logger.info("-------------------- Starting training " f"{pair} --------------------")
# filter the features requested by user in the configuration file and elegantly handle NaNs
features_filtered, labels_filtered = dk.filter_features(
unfiltered_dataframe,
dk.training_features_list,
dk.label_list,
training_filter=True,
)
start_date = unfiltered_dataframe["date"].iloc[0].strftime("%Y-%m-%d")
end_date = unfiltered_dataframe["date"].iloc[-1].strftime("%Y-%m-%d")
logger.info(f"-------------------- Training on data from {start_date} to "
f"{end_date}--------------------")
# split data into train/test data.
data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered)
if not self.freqai_info.get('fit_live_predictions', 0) or not self.live:
dk.fit_labels()
# normalize all data based on train_dataset only
data_dictionary = dk.normalize_data(data_dictionary)
# optional additional data cleaning/analysis
self.data_cleaning_train(dk)
logger.info(
f'Training model on {len(dk.data_dictionary["train_features"].columns)}' " features"
)
logger.info(f'Training model on {len(data_dictionary["train_features"])} data points')
model = self.fit(data_dictionary)
logger.info(f"--------------------done training {pair}--------------------")
return model
def predict(
self, unfiltered_dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False
) -> Tuple[DataFrame, npt.NDArray[np.int_]]:
"""
Filter the prediction features data and predict with it.
:param: unfiltered_dataframe: Full dataframe for the current backtest period.
:return:
:pred_df: dataframe containing the predictions
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
data (NaNs) or felt uncertain about data (PCA and DI index)
"""
dk.find_features(unfiltered_dataframe)
filtered_dataframe, _ = dk.filter_features(
unfiltered_dataframe, dk.training_features_list, training_filter=False
)
filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe)
dk.data_dictionary["prediction_features"] = filtered_dataframe
# optional additional data cleaning/analysis
self.data_cleaning_predict(dk, filtered_dataframe)
predictions = self.model.predict(dk.data_dictionary["prediction_features"])
pred_df = DataFrame(predictions, columns=dk.label_list)
pred_df = dk.denormalize_labels_from_metadata(pred_df)
return (pred_df, dk.do_predict)

View File

@@ -1,64 +0,0 @@
import logging
from typing import Any
from pandas import DataFrame
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from freqtrade.freqai.freqai_interface import IFreqaiModel
logger = logging.getLogger(__name__)
class BaseTensorFlowModel(IFreqaiModel):
"""
Base class for TensorFlow type models.
User *must* inherit from this class and set fit() and predict().
"""
def train(
self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen
) -> Any:
"""
Filter the training data and train a model to it. Train makes heavy use of the datakitchen
for storing, saving, loading, and analyzing the data.
:param unfiltered_dataframe: Full dataframe for the current training period
:param metadata: pair metadata from strategy.
:return:
:model: Trained model which can be used to inference (self.predict)
"""
logger.info("-------------------- Starting training " f"{pair} --------------------")
# filter the features requested by user in the configuration file and elegantly handle NaNs
features_filtered, labels_filtered = dk.filter_features(
unfiltered_dataframe,
dk.training_features_list,
dk.label_list,
training_filter=True,
)
start_date = unfiltered_dataframe["date"].iloc[0].strftime("%Y-%m-%d")
end_date = unfiltered_dataframe["date"].iloc[-1].strftime("%Y-%m-%d")
logger.info(f"-------------------- Training on data from {start_date} to "
f"{end_date}--------------------")
# split data into train/test data.
data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered)
if not self.freqai_info.get('fit_live_predictions', 0) or not self.live:
dk.fit_labels()
# normalize all data based on train_dataset only
data_dictionary = dk.normalize_data(data_dictionary)
# optional additional data cleaning/analysis
self.data_cleaning_train(dk)
logger.info(
f'Training model on {len(dk.data_dictionary["train_features"].columns)}' " features"
)
logger.info(f'Training model on {len(data_dictionary["train_features"])} data points')
model = self.fit(data_dictionary)
logger.info(f"--------------------done training {pair}--------------------")
return model

View File

@@ -1,41 +0,0 @@
import logging
from typing import Any, Dict
from catboost import CatBoostClassifier, Pool
from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel
logger = logging.getLogger(__name__)
class CatboostClassifier(BaseClassifierModel):
"""
User created prediction model. The class needs to override three necessary
functions, predict(), train(), fit(). The class inherits ModelHandler which
has its own DataHandler where data is held, saved, loaded, and managed.
"""
def fit(self, data_dictionary: Dict) -> Any:
"""
User sets up the training and test data to fit their desired model here
:params:
:data_dictionary: the dictionary constructed by DataHandler to hold
all the training and test data/labels.
"""
train_data = Pool(
data=data_dictionary["train_features"],
label=data_dictionary["train_labels"],
weight=data_dictionary["train_weights"],
)
cbr = CatBoostClassifier(
allow_writing_files=False,
loss_function='MultiClass',
**self.model_training_parameters,
)
cbr.fit(train_data)
return cbr

View File

@@ -1,53 +0,0 @@
import gc
import logging
from typing import Any, Dict
from catboost import CatBoostRegressor, Pool
from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel
logger = logging.getLogger(__name__)
class CatboostRegressor(BaseRegressionModel):
"""
User created prediction model. The class needs to override three necessary
functions, predict(), train(), fit(). The class inherits ModelHandler which
has its own DataHandler where data is held, saved, loaded, and managed.
"""
def fit(self, data_dictionary: Dict) -> Any:
"""
User sets up the training and test data to fit their desired model here
:param data_dictionary: the dictionary constructed by DataHandler to hold
all the training and test data/labels.
"""
train_data = Pool(
data=data_dictionary["train_features"],
label=data_dictionary["train_labels"],
weight=data_dictionary["train_weights"],
)
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0:
test_data = None
else:
test_data = Pool(
data=data_dictionary["test_features"],
label=data_dictionary["test_labels"],
weight=data_dictionary["test_weights"],
)
model = CatBoostRegressor(
allow_writing_files=False,
**self.model_training_parameters,
)
model.fit(X=train_data, eval_set=test_data)
# some evidence that catboost pools have memory leaks:
# https://github.com/catboost/catboost/issues/1835
del train_data, test_data
gc.collect()
return model

View File

@@ -1,44 +0,0 @@
import logging
from typing import Any, Dict
from catboost import CatBoostRegressor # , Pool
from sklearn.multioutput import MultiOutputRegressor
from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel
logger = logging.getLogger(__name__)
class CatboostRegressorMultiTarget(BaseRegressionModel):
"""
User created prediction model. The class needs to override three necessary
functions, predict(), train(), fit(). The class inherits ModelHandler which
has its own DataHandler where data is held, saved, loaded, and managed.
"""
def fit(self, data_dictionary: Dict) -> Any:
"""
User sets up the training and test data to fit their desired model here
:param data_dictionary: the dictionary constructed by DataHandler to hold
all the training and test data/labels.
"""
cbr = CatBoostRegressor(
allow_writing_files=False,
**self.model_training_parameters,
)
X = data_dictionary["train_features"]
y = data_dictionary["train_labels"]
eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"])
sample_weight = data_dictionary["train_weights"]
model = MultiOutputRegressor(estimator=cbr)
model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set)
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0:
train_score = model.score(X, y)
test_score = model.score(*eval_set)
logger.info(f"Train score {train_score}, Test score {test_score}")
return model

View File

@@ -1,43 +0,0 @@
import logging
from typing import Any, Dict
from lightgbm import LGBMClassifier
from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel
logger = logging.getLogger(__name__)
class LightGBMClassifier(BaseClassifierModel):
"""
User created prediction model. The class needs to override three necessary
functions, predict(), train(), fit(). The class inherits ModelHandler which
has its own DataHandler where data is held, saved, loaded, and managed.
"""
def fit(self, data_dictionary: Dict) -> Any:
"""
User sets up the training and test data to fit their desired model here
:params:
:data_dictionary: the dictionary constructed by DataHandler to hold
all the training and test data/labels.
"""
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0:
eval_set = None
test_weights = None
else:
eval_set = (data_dictionary["test_features"].to_numpy(),
data_dictionary["test_labels"].to_numpy()[:, 0])
test_weights = data_dictionary["test_weights"]
X = data_dictionary["train_features"].to_numpy()
y = data_dictionary["train_labels"].to_numpy()[:, 0]
train_weights = data_dictionary["train_weights"]
model = LGBMClassifier(**self.model_training_parameters)
model.fit(X=X, y=y, eval_set=eval_set, sample_weight=train_weights,
eval_sample_weight=[test_weights])
return model

View File

@@ -1,43 +0,0 @@
import logging
from typing import Any, Dict
from lightgbm import LGBMRegressor
from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel
logger = logging.getLogger(__name__)
class LightGBMRegressor(BaseRegressionModel):
"""
User created prediction model. The class needs to override three necessary
functions, predict(), train(), fit(). The class inherits ModelHandler which
has its own DataHandler where data is held, saved, loaded, and managed.
"""
def fit(self, data_dictionary: Dict) -> Any:
"""
Most regressors use the same function names and arguments e.g. user
can drop in LGBMRegressor in place of CatBoostRegressor and all data
management will be properly handled by Freqai.
:param data_dictionary: the dictionary constructed by DataHandler to hold
all the training and test data/labels.
"""
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0:
eval_set = None
eval_weights = None
else:
eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"])
eval_weights = data_dictionary["test_weights"]
X = data_dictionary["train_features"]
y = data_dictionary["train_labels"]
train_weights = data_dictionary["train_weights"]
model = LGBMRegressor(**self.model_training_parameters)
model.fit(X=X, y=y, eval_set=eval_set, sample_weight=train_weights,
eval_sample_weight=[eval_weights])
return model

View File

@@ -1,39 +0,0 @@
import logging
from typing import Any, Dict
from lightgbm import LGBMRegressor
from sklearn.multioutput import MultiOutputRegressor
from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel
logger = logging.getLogger(__name__)
class LightGBMRegressorMultiTarget(BaseRegressionModel):
"""
User created prediction model. The class needs to override three necessary
functions, predict(), train(), fit(). The class inherits ModelHandler which
has its own DataHandler where data is held, saved, loaded, and managed.
"""
def fit(self, data_dictionary: Dict) -> Any:
"""
User sets up the training and test data to fit their desired model here
:param data_dictionary: the dictionary constructed by DataHandler to hold
all the training and test data/labels.
"""
lgb = LGBMRegressor(**self.model_training_parameters)
X = data_dictionary["train_features"]
y = data_dictionary["train_labels"]
eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"])
sample_weight = data_dictionary["train_weights"]
model = MultiOutputRegressor(estimator=lgb)
model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set)
train_score = model.score(X, y)
test_score = model.score(*eval_set)
logger.info(f"Train score {train_score}, Test score {test_score}")
return model

View File

@@ -25,14 +25,13 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.exchange.exchange import timeframe_to_next_date
from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin from freqtrade.mixins import LoggingMixin
from freqtrade.persistence import Order, PairLocks, Trade, init_db from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager from freqtrade.rpc import RPCManager
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import FtPrecise
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@@ -150,7 +149,7 @@ class FreqtradeBot(LoggingMixin):
self.check_for_open_trades() self.check_for_open_trades()
self.rpc.cleanup() self.rpc.cleanup()
Trade.commit() cleanup_db()
self.exchange.close() self.exchange.close()
def startup(self) -> None: def startup(self) -> None:
@@ -159,8 +158,6 @@ class FreqtradeBot(LoggingMixin):
performs startup tasks performs startup tasks
""" """
self.rpc.startup_messages(self.config, self.pairlists, self.protections) self.rpc.startup_messages(self.config, self.pairlists, self.protections)
# Update older trades with precision and precision mode
self.startup_backpopulate_precision()
if not self.edge: if not self.edge:
# Adjust stoploss if it was changed # Adjust stoploss if it was changed
Trade.stoploss_reinitialization(self.strategy.stoploss) Trade.stoploss_reinitialization(self.strategy.stoploss)
@@ -217,7 +214,6 @@ class FreqtradeBot(LoggingMixin):
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
self._schedule.run_pending() self._schedule.run_pending()
Trade.commit() Trade.commit()
self.rpc.process_msg_queue(self.dataprovider._msg_queue)
self.last_process = datetime.now(timezone.utc) self.last_process = datetime.now(timezone.utc)
def process_stopped(self) -> None: def process_stopped(self) -> None:
@@ -288,17 +284,6 @@ class FreqtradeBot(LoggingMixin):
else: else:
return 0.0 return 0.0
def startup_backpopulate_precision(self):
trades = Trade.get_trades([Trade.precision_mode.is_(None)])
for trade in trades:
if trade.exchange != self.exchange.id:
continue
trade.precision_mode = self.exchange.precisionMode
trade.amount_precision = self.exchange.get_precision_amount(trade.pair)
trade.price_precision = self.exchange.get_precision_price(trade.pair)
Trade.commit()
def startup_update_open_orders(self): def startup_update_open_orders(self):
""" """
Updates open orders based on order list kept in the database. Updates open orders based on order list kept in the database.
@@ -418,7 +403,7 @@ class FreqtradeBot(LoggingMixin):
whitelist = copy.deepcopy(self.active_pair_whitelist) whitelist = copy.deepcopy(self.active_pair_whitelist)
if not whitelist: if not whitelist:
self.log_once("Active pair whitelist is empty.", logger.info) logger.info("Active pair whitelist is empty.")
return trades_created return trades_created
# Remove pairs for currently opened trades from the whitelist # Remove pairs for currently opened trades from the whitelist
for trade in Trade.get_open_trades(): for trade in Trade.get_open_trades():
@@ -427,8 +412,8 @@ class FreqtradeBot(LoggingMixin):
logger.debug('Ignoring %s in pair whitelist', trade.pair) logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist: if not whitelist:
self.log_once("No currency pair in active pair whitelist, " logger.info("No currency pair in active pair whitelist, "
"but checking to exit open trades.", logger.info) "but checking to exit open trades.")
return trades_created return trades_created
if PairLocks.is_global_lock(side='*'): if PairLocks.is_global_lock(side='*'):
# This only checks for total locks (both sides). # This only checks for total locks (both sides).
@@ -539,61 +524,39 @@ class FreqtradeBot(LoggingMixin):
If the strategy triggers the adjustment, a new order gets issued. If the strategy triggers the adjustment, a new order gets issued.
Once that completes, the existing trade is modified to match new data. Once that completes, the existing trade is modified to match new data.
""" """
current_entry_rate, current_exit_rate = self.exchange.get_rates(
trade.pair, True, trade.is_short)
current_entry_profit = trade.calc_profit_ratio(current_entry_rate)
current_exit_profit = trade.calc_profit_ratio(current_exit_rate)
min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
current_entry_rate,
self.strategy.stoploss)
min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
current_exit_rate,
self.strategy.stoploss)
max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate)
stake_available = self.wallets.get_available_stake_amount()
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
default_retval=None)(
trade=trade,
current_time=datetime.now(timezone.utc), current_rate=current_entry_rate,
current_profit=current_entry_profit, min_stake=min_entry_stake,
max_stake=min(max_entry_stake, stake_available),
current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate,
current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit
)
if stake_amount is not None and stake_amount > 0.0:
# We should increase our position
if self.strategy.max_entry_position_adjustment > -1: if self.strategy.max_entry_position_adjustment > -1:
count_of_entries = trade.nr_of_successful_entries count_of_buys = trade.nr_of_successful_entries
if count_of_entries > self.strategy.max_entry_position_adjustment: if count_of_buys > self.strategy.max_entry_position_adjustment:
logger.debug(f"Max adjustment entries for {trade.pair} has been reached.") logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
return return
else: else:
logger.debug("Max adjustment entries is set to unlimited.") logger.debug("Max adjustment entries is set to unlimited.")
self.execute_entry(trade.pair, stake_amount, price=current_entry_rate, current_rate = self.exchange.get_rate(
trade.pair, side='entry', is_short=trade.is_short, refresh=True)
current_profit = trade.calc_profit_ratio(current_rate)
min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair,
current_rate,
self.strategy.stoploss)
max_stake_amount = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
stake_available = self.wallets.get_available_stake_amount()
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
default_retval=None)(
trade=trade, current_time=datetime.now(timezone.utc), current_rate=current_rate,
current_profit=current_profit, min_stake=min_stake_amount,
max_stake=min(max_stake_amount, stake_available))
if stake_amount is not None and stake_amount > 0.0:
# We should increase our position
self.execute_entry(trade.pair, stake_amount, price=current_rate,
trade=trade, is_short=trade.is_short) trade=trade, is_short=trade.is_short)
if stake_amount is not None and stake_amount < 0.0: if stake_amount is not None and stake_amount < 0.0:
# We should decrease our position # We should decrease our position
amount = abs(float(FtPrecise(stake_amount) / FtPrecise(current_exit_rate))) # TODO: Selling part of the trade not implemented yet.
if amount > trade.amount: logger.error(f"Unable to decrease trade position / sell partially"
# This is currently ineffective as remaining would become < min tradable f" for pair {trade.pair}, feature not implemented.")
# Fixing this would require checking for 0.0 there -
# if we decide that this callback is allowed to "fully exit"
logger.info(
f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}")
amount = trade.amount
remaining = (trade.amount - amount) * current_exit_rate
if remaining < min_exit_stake:
logger.info(f'Remaining amount of {remaining} would be too small.')
return
self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple(
exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount)
def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool: def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool:
""" """
@@ -637,8 +600,7 @@ class FreqtradeBot(LoggingMixin):
ordertype: Optional[str] = None, ordertype: Optional[str] = None,
enter_tag: Optional[str] = None, enter_tag: Optional[str] = None,
trade: Optional[Trade] = None, trade: Optional[Trade] = None,
order_adjust: bool = False, order_adjust: bool = False
leverage_: Optional[float] = None,
) -> bool: ) -> bool:
""" """
Executes a limit buy for the given pair Executes a limit buy for the given pair
@@ -654,7 +616,7 @@ class FreqtradeBot(LoggingMixin):
pos_adjust = trade is not None pos_adjust = trade is not None
enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_) pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust)
if not stake_amount: if not stake_amount:
return False return False
@@ -751,10 +713,7 @@ class FreqtradeBot(LoggingMixin):
leverage=leverage, leverage=leverage,
is_short=is_short, is_short=is_short,
trading_mode=self.trading_mode, trading_mode=self.trading_mode,
funding_fees=funding_fees, funding_fees=funding_fees
amount_precision=self.exchange.get_precision_amount(pair),
price_precision=self.exchange.get_precision_price(pair),
precision_mode=self.exchange.precisionMode,
) )
else: else:
# This is additional buy, we reset fee_open_currency so timeout checking can work # This is additional buy, we reset fee_open_currency so timeout checking can work
@@ -771,7 +730,7 @@ class FreqtradeBot(LoggingMixin):
# Updating wallets # Updating wallets
self.wallets.update() self.wallets.update()
self._notify_enter(trade, order_obj, order_type, sub_trade=pos_adjust) self._notify_enter(trade, order, order_type)
if pos_adjust: if pos_adjust:
if order_status == 'closed': if order_status == 'closed':
@@ -780,8 +739,8 @@ class FreqtradeBot(LoggingMixin):
else: else:
logger.info(f"DCA order {order_status}, will wait for resolution: {trade}") logger.info(f"DCA order {order_status}, will wait for resolution: {trade}")
# Update fees if order is non-opened # Update fees if order is closed
if order_status in constants.NON_OPEN_EXCHANGE_STATES: if order_status == 'closed':
self.update_trade_state(trade, order_id, order) self.update_trade_state(trade, order_id, order)
return True return True
@@ -804,7 +763,6 @@ class FreqtradeBot(LoggingMixin):
entry_tag: Optional[str], entry_tag: Optional[str],
trade: Optional[Trade], trade: Optional[Trade],
order_adjust: bool, order_adjust: bool,
leverage_: Optional[float],
) -> Tuple[float, float, float]: ) -> Tuple[float, float, float]:
if price: if price:
@@ -827,11 +785,8 @@ class FreqtradeBot(LoggingMixin):
if not enter_limit_requested: if not enter_limit_requested:
raise PricingError('Could not determine entry price.') raise PricingError('Could not determine entry price.')
if self.trading_mode != TradingMode.SPOT and trade is None: if trade is None:
max_leverage = self.exchange.get_max_leverage(pair, stake_amount) max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
if leverage_:
leverage = leverage_
else:
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)( leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair, pair=pair,
current_time=datetime.now(timezone.utc), current_time=datetime.now(timezone.utc),
@@ -839,7 +794,7 @@ class FreqtradeBot(LoggingMixin):
proposed_leverage=1.0, proposed_leverage=1.0,
max_leverage=max_leverage, max_leverage=max_leverage,
side=trade_side, entry_tag=entry_tag, side=trade_side, entry_tag=entry_tag,
) ) if self.trading_mode != TradingMode.SPOT else 1.0
# Cap leverage between 1.0 and max_leverage. # Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage) leverage = min(max(leverage, 1.0), max_leverage)
else: else:
@@ -874,14 +829,13 @@ class FreqtradeBot(LoggingMixin):
return enter_limit_requested, stake_amount, leverage return enter_limit_requested, stake_amount, leverage
def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str] = None, def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None,
fill: bool = False, sub_trade: bool = False) -> None: fill: bool = False) -> None:
""" """
Sends rpc notification when a entry order occurred. Sends rpc notification when a entry order occurred.
""" """
msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY
open_rate = order.safe_price open_rate = safe_value_fallback(order, 'average', 'price')
if open_rate is None: if open_rate is None:
open_rate = trade.open_rate open_rate = trade.open_rate
@@ -905,17 +859,15 @@ class FreqtradeBot(LoggingMixin):
'stake_amount': trade.stake_amount, 'stake_amount': trade.stake_amount,
'stake_currency': self.config['stake_currency'], 'stake_currency': self.config['stake_currency'],
'fiat_currency': self.config.get('fiat_display_currency', None), 'fiat_currency': self.config.get('fiat_display_currency', None),
'amount': order.safe_amount_after_fee, 'amount': safe_value_fallback(order, 'filled', 'amount') or trade.amount,
'open_date': trade.open_date or datetime.utcnow(), 'open_date': trade.open_date or datetime.utcnow(),
'current_rate': current_rate, 'current_rate': current_rate,
'sub_trade': sub_trade,
} }
# Send the message # Send the message
self.rpc.send_msg(msg) self.rpc.send_msg(msg)
def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str, def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
sub_trade: bool = False) -> None:
""" """
Sends rpc notification when a entry order cancel occurred. Sends rpc notification when a entry order cancel occurred.
""" """
@@ -940,7 +892,6 @@ class FreqtradeBot(LoggingMixin):
'open_date': trade.open_date, 'open_date': trade.open_date,
'current_rate': current_rate, 'current_rate': current_rate,
'reason': reason, 'reason': reason,
'sub_trade': sub_trade,
} }
# Send the message # Send the message
@@ -1064,7 +1015,7 @@ class FreqtradeBot(LoggingMixin):
trade.stoploss_order_id = None trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Exiting the trade forcefully') logger.warning('Exiting the trade forcefully')
self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple( self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple(
exit_type=ExitType.EMERGENCY_EXIT)) exit_type=ExitType.EMERGENCY_EXIT))
except ExchangeError: except ExchangeError:
@@ -1134,7 +1085,7 @@ class FreqtradeBot(LoggingMixin):
if (trade.is_open if (trade.is_open
and stoploss_order and stoploss_order
and stoploss_order['status'] in ('canceled', 'cancelled')): and stoploss_order['status'] in ('canceled', 'cancelled')):
if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation): if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss):
return False return False
else: else:
trade.stoploss_order_id = None trade.stoploss_order_id = None
@@ -1163,7 +1114,7 @@ class FreqtradeBot(LoggingMixin):
:param order: Current on exchange stoploss order :param order: Current on exchange stoploss order
:return: None :return: None
""" """
stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation) stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stop_loss)
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side): if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
# we check if the update is necessary # we check if the update is necessary
@@ -1181,7 +1132,7 @@ class FreqtradeBot(LoggingMixin):
f"for pair {trade.pair}") f"for pair {trade.pair}")
# Create new stoploss order # Create new stoploss order
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm): if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss):
logger.warning(f"Could not create trailing stoploss order " logger.warning(f"Could not create trailing stoploss order "
f"for pair {trade.pair}.") f"for pair {trade.pair}.")
@@ -1414,22 +1365,16 @@ class FreqtradeBot(LoggingMixin):
trade.open_order_id = None trade.open_order_id = None
trade.exit_reason = None trade.exit_reason = None
cancelled = True cancelled = True
self.wallets.update()
else: else:
# TODO: figure out how to handle partially complete sell orders # TODO: figure out how to handle partially complete sell orders
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
cancelled = False cancelled = False
order_obj = trade.select_order_by_order_id(order['id']) self.wallets.update()
if not order_obj:
raise DependencyException(
f"Order_obj not found for {order['id']}. This should not have happened.")
sub_trade = order_obj.amount != trade.amount
self._notify_exit_cancel( self._notify_exit_cancel(
trade, trade,
order_type=self.strategy.order_types['exit'], order_type=self.strategy.order_types['exit'],
reason=reason, order=order_obj, sub_trade=sub_trade reason=reason
) )
return cancelled return cancelled
@@ -1470,7 +1415,6 @@ class FreqtradeBot(LoggingMixin):
*, *,
exit_tag: Optional[str] = None, exit_tag: Optional[str] = None,
ordertype: Optional[str] = None, ordertype: Optional[str] = None,
sub_trade_amt: float = None,
) -> bool: ) -> bool:
""" """
Executes a trade exit for the given trade and limit Executes a trade exit for the given trade and limit
@@ -1487,10 +1431,15 @@ class FreqtradeBot(LoggingMixin):
) )
exit_type = 'exit' exit_type = 'exit'
exit_reason = exit_tag or exit_check.exit_reason exit_reason = exit_tag or exit_check.exit_reason
if exit_check.exit_type in ( if exit_check.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION):
exit_type = 'stoploss' exit_type = 'stoploss'
# if stoploss is on exchange and we are on dry_run mode,
# we consider the sell price stop price
if (self.config['dry_run'] and exit_type == 'stoploss'
and self.strategy.order_types['stoploss_on_exchange']):
limit = trade.stop_loss
# set custom_exit_price if available # set custom_exit_price if available
proposed_limit_rate = limit proposed_limit_rate = limit
current_profit = trade.calc_profit_ratio(limit) current_profit = trade.calc_profit_ratio(limit)
@@ -1511,17 +1460,14 @@ class FreqtradeBot(LoggingMixin):
# Emergency sells (default to market!) # Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergency_exit", "market") order_type = self.strategy.order_types.get("emergency_exit", "market")
amount = self._safe_exit_amount(trade.pair, sub_trade_amt or trade.amount) amount = self._safe_exit_amount(trade.pair, trade.amount)
time_in_force = self.strategy.order_time_in_force['exit'] time_in_force = self.strategy.order_time_in_force['exit']
if (exit_check.exit_type != ExitType.LIQUIDATION if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
and not sub_trade_amt
and not strategy_safe_wrapper(
self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
time_in_force=time_in_force, exit_reason=exit_reason, time_in_force=time_in_force, exit_reason=exit_reason,
sell_reason=exit_reason, # sellreason -> compatibility sell_reason=exit_reason, # sellreason -> compatibility
current_time=datetime.now(timezone.utc))): current_time=datetime.now(timezone.utc)):
logger.info(f"User denied exit for {trade.pair}.") logger.info(f"User denied exit for {trade.pair}.")
return False return False
@@ -1555,7 +1501,7 @@ class FreqtradeBot(LoggingMixin):
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
reason='Auto lock') reason='Auto lock')
self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj) self._notify_exit(trade, order_type)
# In case of market sell orders the order can be closed immediately # In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') in ('closed', 'expired'): if order.get('status', 'unknown') in ('closed', 'expired'):
self.update_trade_state(trade, trade.open_order_id, order) self.update_trade_state(trade, trade.open_order_id, order)
@@ -1563,27 +1509,16 @@ class FreqtradeBot(LoggingMixin):
return True return True
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False, def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None:
sub_trade: bool = False, order: Order = None) -> None:
""" """
Sends rpc notification when a sell occurred. Sends rpc notification when a sell occurred.
""" """
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate)
# Use cached rates here - it was updated seconds ago. # Use cached rates here - it was updated seconds ago.
current_rate = self.exchange.get_rate( current_rate = self.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=False) if not fill else None trade.pair, side='exit', is_short=trade.is_short, refresh=False) if not fill else None
# second condition is for mypy only; order will always be passed during sub trade
if sub_trade and order is not None:
amount = order.safe_filled if fill else order.amount
profit_rate = order.safe_price
profit = trade.calc_profit(rate=profit_rate, amount=amount, open_rate=trade.open_rate)
profit_ratio = trade.calc_profit_ratio(profit_rate, amount, trade.open_rate)
else:
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit = trade.calc_profit(rate=profit_rate) + (0.0 if fill else trade.realized_profit)
profit_ratio = trade.calc_profit_ratio(profit_rate) profit_ratio = trade.calc_profit_ratio(profit_rate)
amount = trade.amount
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"
msg = { msg = {
@@ -1597,11 +1532,11 @@ class FreqtradeBot(LoggingMixin):
'gain': gain, 'gain': gain,
'limit': profit_rate, 'limit': profit_rate,
'order_type': order_type, 'order_type': order_type,
'amount': amount, 'amount': trade.amount,
'open_rate': trade.open_rate, 'open_rate': trade.open_rate,
'close_rate': profit_rate, 'close_rate': trade.close_rate,
'current_rate': current_rate, 'current_rate': current_rate,
'profit_amount': profit, 'profit_amount': profit_trade,
'profit_ratio': profit_ratio, 'profit_ratio': profit_ratio,
'buy_tag': trade.enter_tag, 'buy_tag': trade.enter_tag,
'enter_tag': trade.enter_tag, 'enter_tag': trade.enter_tag,
@@ -1609,18 +1544,19 @@ class FreqtradeBot(LoggingMixin):
'exit_reason': trade.exit_reason, 'exit_reason': trade.exit_reason,
'open_date': trade.open_date, 'open_date': trade.open_date,
'close_date': trade.close_date or datetime.utcnow(), 'close_date': trade.close_date or datetime.utcnow(),
'stake_amount': trade.stake_amount,
'stake_currency': self.config['stake_currency'], 'stake_currency': self.config['stake_currency'],
'fiat_currency': self.config.get('fiat_display_currency'), 'fiat_currency': self.config.get('fiat_display_currency'),
'sub_trade': sub_trade,
'cumulative_profit': trade.realized_profit,
} }
if 'fiat_display_currency' in self.config:
msg.update({
'fiat_currency': self.config['fiat_display_currency'],
})
# Send the message # Send the message
self.rpc.send_msg(msg) self.rpc.send_msg(msg)
def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str, def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
order: Order, sub_trade: bool = False) -> None:
""" """
Sends rpc notification when a sell cancel occurred. Sends rpc notification when a sell cancel occurred.
""" """
@@ -1646,7 +1582,7 @@ class FreqtradeBot(LoggingMixin):
'gain': gain, 'gain': gain,
'limit': profit_rate or 0, 'limit': profit_rate or 0,
'order_type': order_type, 'order_type': order_type,
'amount': order.safe_amount_after_fee, 'amount': trade.amount,
'open_rate': trade.open_rate, 'open_rate': trade.open_rate,
'current_rate': current_rate, 'current_rate': current_rate,
'profit_amount': profit_trade, 'profit_amount': profit_trade,
@@ -1660,8 +1596,6 @@ class FreqtradeBot(LoggingMixin):
'stake_currency': self.config['stake_currency'], 'stake_currency': self.config['stake_currency'],
'fiat_currency': self.config.get('fiat_display_currency', None), 'fiat_currency': self.config.get('fiat_display_currency', None),
'reason': reason, 'reason': reason,
'sub_trade': sub_trade,
'stake_amount': trade.stake_amount,
} }
if 'fiat_display_currency' in self.config: if 'fiat_display_currency' in self.config:
@@ -1716,50 +1650,40 @@ class FreqtradeBot(LoggingMixin):
self.handle_order_fee(trade, order_obj, order) self.handle_order_fee(trade, order_obj, order)
trade.update_trade(order_obj) trade.update_trade(order_obj)
# TODO: is the below necessary? it's already done in update_trade for filled buys
trade.recalc_trade_from_orders()
Trade.commit()
if order.get('status') in constants.NON_OPEN_EXCHANGE_STATES: if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
# If a entry order was closed, force update on stoploss on exchange # If a entry order was closed, force update on stoploss on exchange
if order.get('side') == trade.entry_side: if order.get('side') == trade.entry_side:
trade = self.cancel_stoploss_on_exchange(trade) trade = self.cancel_stoploss_on_exchange(trade)
if not self.edge:
# TODO: should shorting/leverage be supported by Edge,
# then this will need to be fixed.
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
if order.get('side') == trade.entry_side or trade.amount > 0:
# Must also run for partial exits
# TODO: Margin will need to use interest_rate as well. # TODO: Margin will need to use interest_rate as well.
# interest_rate = self.exchange.get_interest_rate() # interest_rate = self.exchange.get_interest_rate()
trade.set_liquidation_price(self.exchange.get_liquidation_price( trade.set_isolated_liq(self.exchange.get_liquidation_price(
leverage=trade.leverage, leverage=trade.leverage,
pair=trade.pair, pair=trade.pair,
amount=trade.amount, amount=trade.amount,
open_rate=trade.open_rate, open_rate=trade.open_rate,
is_short=trade.is_short is_short=trade.is_short
)) ))
if not self.edge:
# TODO: should shorting/leverage be supported by Edge,
# then this will need to be fixed.
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
# Updating wallets when order is closed # Updating wallets when order is closed
self.wallets.update() self.wallets.update()
Trade.commit()
self.order_close_notify(trade, order_obj, stoploss_order, send_msg)
return False
def order_close_notify(
self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool):
"""send "fill" notifications"""
sub_trade = not isclose(order.safe_amount_after_fee,
trade.amount, abs_tol=constants.MATH_CLOSE_PREC)
if order.ft_order_side == trade.exit_side:
# Exit notification
if send_msg and not stoploss_order and not trade.open_order_id:
self._notify_exit(trade, '', fill=True, sub_trade=sub_trade, order=order)
if not trade.is_open: if not trade.is_open:
if send_msg and not stoploss_order and not trade.open_order_id:
self._notify_exit(trade, '', True)
self.handle_protections(trade.pair, trade.trade_direction) self.handle_protections(trade.pair, trade.trade_direction)
elif send_msg and not trade.open_order_id and not stoploss_order: elif send_msg and not trade.open_order_id and not stoploss_order:
# Enter fill # Enter fill
self._notify_enter(trade, order, fill=True, sub_trade=sub_trade) self._notify_enter(trade, order, fill=True)
return False
def handle_protections(self, pair: str, side: LongShort) -> None: def handle_protections(self, pair: str, side: LongShort) -> None:
prot_trig = self.protections.stop_per_pair(pair, side=side) prot_trig = self.protections.stop_per_pair(pair, side=side)
@@ -1882,9 +1806,6 @@ class FreqtradeBot(LoggingMixin):
if fee_rate is not None and fee_rate < 0.02: if fee_rate is not None and fee_rate < 0.02:
# Only update if fee-rate is < 2% # Only update if fee-rate is < 2%
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
else:
logger.warning(
f"Not updating {order.get('side', '')}-fee - rate: {fee_rate}, {fee_currency}.")
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
# * Leverage could be a cause for this warning # * Leverage could be a cause for this warning

View File

@@ -1,20 +1,20 @@
from decimal import Decimal
from math import ceil from math import ceil
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.util import FtPrecise
one = FtPrecise(1.0) one = Decimal(1.0)
four = FtPrecise(4.0) four = Decimal(4.0)
twenty_four = FtPrecise(24.0) twenty_four = Decimal(24.0)
def interest( def interest(
exchange_name: str, exchange_name: str,
borrowed: FtPrecise, borrowed: Decimal,
rate: FtPrecise, rate: Decimal,
hours: FtPrecise hours: Decimal
) -> FtPrecise: ) -> Decimal:
""" """
Equation to calculate interest on margin trades Equation to calculate interest on margin trades
@@ -31,13 +31,13 @@ def interest(
""" """
exchange_name = exchange_name.lower() exchange_name = exchange_name.lower()
if exchange_name == "binance": if exchange_name == "binance":
return borrowed * rate * FtPrecise(ceil(hours)) / twenty_four return borrowed * rate * ceil(hours) / twenty_four
elif exchange_name == "kraken": elif exchange_name == "kraken":
# Rounded based on https://kraken-fees-calculator.github.io/ # Rounded based on https://kraken-fees-calculator.github.io/
return borrowed * rate * (one + FtPrecise(ceil(hours / four))) return borrowed * rate * (one + ceil(hours / four))
elif exchange_name == "ftx": elif exchange_name == "ftx":
# As Explained under #Interest rates section in # As Explained under #Interest rates section in
# https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer
return borrowed * rate * FtPrecise(ceil(hours)) / twenty_four return borrowed * rate * ceil(hours) / twenty_four
else: else:
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")

98
freqtrade/optimize/backtesting.py Normal file → Executable file
View File

@@ -89,9 +89,6 @@ class Backtesting:
self.dataprovider = DataProvider(self.config, self.exchange) self.dataprovider = DataProvider(self.config, self.exchange)
if self.config.get('strategy_list'): if self.config.get('strategy_list'):
if self.config.get('freqai', {}).get('enabled', False):
raise OperationalException(
"You can't use strategy_list and freqai at the same time.")
for strat in list(self.config['strategy_list']): for strat in list(self.config['strategy_list']):
stratconf = deepcopy(self.config) stratconf = deepcopy(self.config)
stratconf['strategy'] = strat stratconf['strategy'] = strat
@@ -131,7 +128,6 @@ class Backtesting:
self.fee = config['fee'] self.fee = config['fee']
else: else:
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
self.precision_mode = self.exchange.precisionMode
self.timerange = TimeRange.parse_timerange( self.timerange = TimeRange.parse_timerange(
None if self.config.get('timerange') is None else str(self.config.get('timerange'))) None if self.config.get('timerange') is None else str(self.config.get('timerange')))
@@ -211,15 +207,6 @@ class Backtesting:
""" """
self.progress.init_step(BacktestState.DATALOAD, 1) self.progress.init_step(BacktestState.DATALOAD, 1)
if self.config.get('freqai', {}).get('enabled', False):
startup_candles = int(self.config.get('freqai', {}).get('startup_candles', 0))
if not startup_candles:
raise OperationalException('FreqAI backtesting module requires user set '
'startup_candles in config.')
self.required_startup += int(self.config.get('freqai', {}).get('startup_candles', 0))
logger.info(f'Increasing startup_candle_count for freqai to {self.required_startup}')
self.config['startup_candle_count'] = self.required_startup
data = history.load_data( data = history.load_data(
datadir=self.config['datadir'], datadir=self.config['datadir'],
pairs=self.pairlists.whitelist, pairs=self.pairlists.whitelist,
@@ -394,8 +381,7 @@ class Backtesting:
Get close rate for backtesting result Get close rate for backtesting result
""" """
# Special handling if high or low hit STOP_LOSS or ROI # Special handling if high or low hit STOP_LOSS or ROI
if exit.exit_type in ( if exit.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION):
return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur) return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur)
elif exit.exit_type == (ExitType.ROI): elif exit.exit_type == (ExitType.ROI):
return self._get_close_rate_for_roi(row, trade, exit, trade_dur) return self._get_close_rate_for_roi(row, trade, exit, trade_dur)
@@ -410,16 +396,11 @@ class Backtesting:
is_short = trade.is_short or False is_short = trade.is_short or False
leverage = trade.leverage or 1.0 leverage = trade.leverage or 1.0
side_1 = -1 if is_short else 1 side_1 = -1 if is_short else 1
if exit.exit_type == ExitType.LIQUIDATION and trade.liquidation_price:
stoploss_value = trade.liquidation_price
else:
stoploss_value = trade.stop_loss
if is_short: if is_short:
if stoploss_value < row[LOW_IDX]: if trade.stop_loss < row[LOW_IDX]:
return row[OPEN_IDX] return row[OPEN_IDX]
else: else:
if stoploss_value > row[HIGH_IDX]: if trade.stop_loss > row[HIGH_IDX]:
return row[OPEN_IDX] return row[OPEN_IDX]
# Special case: trailing triggers within same candle as trade opened. Assume most # Special case: trailing triggers within same candle as trade opened. Assume most
@@ -452,7 +433,7 @@ class Backtesting:
return max(row[LOW_IDX], stop_rate) return max(row[LOW_IDX], stop_rate)
# Set close_rate to stoploss # Set close_rate to stoploss
return stoploss_value return trade.stop_loss
def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple, def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple,
trade_dur: int) -> float: trade_dur: int) -> float:
@@ -516,20 +497,16 @@ class Backtesting:
def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple
) -> LocalTrade: ) -> LocalTrade:
current_rate = row[OPEN_IDX] current_profit = trade.calc_profit_ratio(row[OPEN_IDX])
current_date = row[DATE_IDX].to_pydatetime() min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, row[OPEN_IDX], -0.1)
current_profit = trade.calc_profit_ratio(current_rate) max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, row[OPEN_IDX])
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1)
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
stake_available = self.wallets.get_available_stake_amount() stake_available = self.wallets.get_available_stake_amount()
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
default_retval=None)( default_retval=None)(
trade=trade, # type: ignore[arg-type] trade=trade, # type: ignore[arg-type]
current_time=current_date, current_rate=current_rate, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
current_profit=current_profit, min_stake=min_stake, current_profit=current_profit, min_stake=min_stake,
max_stake=min(max_stake, stake_available), max_stake=min(max_stake, stake_available))
current_entry_rate=current_rate, current_exit_rate=current_rate,
current_entry_profit=current_profit, current_exit_profit=current_profit)
# Check if we should increase our position # Check if we should increase our position
if stake_amount is not None and stake_amount > 0.0: if stake_amount is not None and stake_amount > 0.0:
@@ -540,24 +517,6 @@ class Backtesting:
self.wallets.update() self.wallets.update()
return pos_trade return pos_trade
if stake_amount is not None and stake_amount < 0.0:
amount = abs(stake_amount) / current_rate
if amount > trade.amount:
# This is currently ineffective as remaining would become < min tradable
amount = trade.amount
remaining = (trade.amount - amount) * current_rate
if remaining < min_stake:
# Remaining stake is too low to be sold.
return trade
pos_trade = self._exit_trade(trade, row, current_rate, amount)
if pos_trade is not None:
order = pos_trade.orders[-1]
if self._get_order_filled(order.price, row):
order.close_bt_order(current_date, trade)
trade.recalc_trade_from_orders()
self.wallets.update()
return pos_trade
return trade return trade
def _get_order_filled(self, rate: float, row: Tuple) -> bool: def _get_order_filled(self, rate: float, row: Tuple) -> bool:
@@ -608,7 +567,7 @@ class Backtesting:
if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT): if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT):
# Checks and adds an exit tag, after checking that the length of the # Checks and adds an exit tag, after checking that the length of the
# row has the length for an exit tag column # row has the length for an exit tag column
if ( if(
len(row) > EXIT_TAG_IDX len(row) > EXIT_TAG_IDX
and row[EXIT_TAG_IDX] is not None and row[EXIT_TAG_IDX] is not None
and len(row[EXIT_TAG_IDX]) > 0 and len(row[EXIT_TAG_IDX]) > 0
@@ -633,30 +592,21 @@ class Backtesting:
# Confirm trade exit: # Confirm trade exit:
time_in_force = self.strategy.order_time_in_force['exit'] time_in_force = self.strategy.order_time_in_force['exit']
if (exit_.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper( if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, pair=trade.pair,
trade=trade, # type: ignore[arg-type] trade=trade, # type: ignore[arg-type]
order_type=order_type, order_type='limit',
amount=trade.amount, amount=trade.amount,
rate=close_rate, rate=close_rate,
time_in_force=time_in_force, time_in_force=time_in_force,
sell_reason=exit_reason, # deprecated sell_reason=exit_reason, # deprecated
exit_reason=exit_reason, exit_reason=exit_reason,
current_time=exit_candle_time)): current_time=exit_candle_time):
return None return None
trade.exit_reason = exit_reason trade.exit_reason = exit_reason
return self._exit_trade(trade, row, close_rate, trade.amount)
return None
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple,
close_rate: float, amount: float = None) -> Optional[LocalTrade]:
self.order_id_counter += 1 self.order_id_counter += 1
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
order_type = self.strategy.order_types['exit']
amount = amount or trade.amount
order = Order( order = Order(
id=self.order_id_counter, id=self.order_id_counter,
ft_trade_id=trade.id, ft_trade_id=trade.id,
@@ -672,14 +622,16 @@ class Backtesting:
status="open", status="open",
price=close_rate, price=close_rate,
average=close_rate, average=close_rate,
amount=amount, amount=trade.amount,
filled=0, filled=0,
remaining=amount, remaining=trade.amount,
cost=amount * close_rate, cost=trade.amount * close_rate,
) )
trade.orders.append(order) trade.orders.append(order)
return trade return trade
return None
def _get_exit_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: def _get_exit_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime() exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
@@ -850,15 +802,12 @@ class Backtesting:
trading_mode=self.trading_mode, trading_mode=self.trading_mode,
leverage=leverage, leverage=leverage,
# interest_rate=interest_rate, # interest_rate=interest_rate,
amount_precision=self.exchange.get_precision_amount(pair),
price_precision=self.exchange.get_precision_price(pair),
precision_mode=self.precision_mode,
orders=[], orders=[],
) )
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
trade.set_liquidation_price(self.exchange.get_liquidation_price( trade.set_isolated_liq(self.exchange.get_liquidation_price(
pair=pair, pair=pair,
open_rate=propose_rate, open_rate=propose_rate,
amount=amount, amount=amount,
@@ -909,8 +858,6 @@ class Backtesting:
# Ignore trade if entry-order did not fill yet # Ignore trade if entry-order did not fill yet
continue continue
exit_row = data[pair][-1] exit_row = data[pair][-1]
self._exit_trade(trade, exit_row, exit_row[OPEN_IDX], trade.amount)
trade.orders[-1].close_bt_order(exit_row[DATE_IDX].to_pydatetime(), trade)
trade.close_date = exit_row[DATE_IDX].to_pydatetime() trade.close_date = exit_row[DATE_IDX].to_pydatetime()
trade.exit_reason = ExitType.FORCE_EXIT.value trade.exit_reason = ExitType.FORCE_EXIT.value
@@ -1052,7 +999,7 @@ class Backtesting:
return None return None
return row return row
def backtest(self, processed: Dict, # noqa: max-complexity: 13 def backtest(self, processed: Dict,
start_date: datetime, end_date: datetime, start_date: datetime, end_date: datetime,
max_open_trades: int = 0, position_stacking: bool = False, max_open_trades: int = 0, position_stacking: bool = False,
enable_protections: bool = False) -> Dict[str, Any]: enable_protections: bool = False) -> Dict[str, Any]:
@@ -1154,11 +1101,6 @@ class Backtesting:
if order and self._get_order_filled(order.price, row): if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time, trade) order.close_bt_order(current_time, trade)
trade.open_order_id = None trade.open_order_id = None
sub_trade = order.safe_amount_after_fee != trade.amount
if sub_trade:
order.close_bt_order(current_time, trade)
trade.recalc_trade_from_orders()
else:
trade.close_date = current_time trade.close_date = current_time
trade.close(order.price, show_msg=False) trade.close(order.price, show_msg=False)

View File

@@ -483,7 +483,6 @@ class Hyperopt:
self.backtesting.exchange._api_async = None self.backtesting.exchange._api_async = None
self.backtesting.exchange.loop = None # type: ignore self.backtesting.exchange.loop = None # type: ignore
self.backtesting.exchange._loop_lock = None # type: ignore self.backtesting.exchange._loop_lock = None # type: ignore
self.backtesting.exchange._cache_lock = None # type: ignore
# self.backtesting.exchange = None # type: ignore # self.backtesting.exchange = None # type: ignore
self.backtesting.pairlists = None # type: ignore self.backtesting.pairlists = None # type: ignore

View File

@@ -639,7 +639,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
:param stake_currency: stake-currency - used to correctly name headers :param stake_currency: stake-currency - used to correctly name headers
:return: pretty printed table with tabulate as string :return: pretty printed table with tabulate as string
""" """
if (tag_type == "enter_tag"): if(tag_type == "enter_tag"):
headers = _get_line_header("TAG", stake_currency) headers = _get_line_header("TAG", stake_currency)
else: else:
headers = _get_line_header("TAG", stake_currency, 'Sells') headers = _get_line_header("TAG", stake_currency, 'Sells')

View File

@@ -1,5 +1,5 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.persistence.models import init_db from freqtrade.persistence.models import cleanup_db, init_db
from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.pairlock_middleware import PairLocks
from freqtrade.persistence.trade_model import LocalTrade, Order, Trade from freqtrade.persistence.trade_model import LocalTrade, Order, Trade

View File

@@ -95,7 +95,6 @@ def migrate_trades_and_orders_table(
exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null')) exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null'))
strategy = get_column_def(cols, 'strategy', 'null') strategy = get_column_def(cols, 'strategy', 'null')
enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null')) enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null'))
realized_profit = get_column_def(cols, 'realized_profit', '0.0')
trading_mode = get_column_def(cols, 'trading_mode', 'null') trading_mode = get_column_def(cols, 'trading_mode', 'null')
@@ -130,10 +129,6 @@ def migrate_trades_and_orders_table(
get_column_def(cols, 'sell_order_status', 'null')) get_column_def(cols, 'sell_order_status', 'null'))
amount_requested = get_column_def(cols, 'amount_requested', 'amount') amount_requested = get_column_def(cols, 'amount_requested', 'amount')
amount_precision = get_column_def(cols, 'amount_precision', 'null')
price_precision = get_column_def(cols, 'price_precision', 'null')
precision_mode = get_column_def(cols, 'precision_mode', 'null')
# Schema migration necessary # Schema migration necessary
with engine.begin() as connection: with engine.begin() as connection:
connection.execute(text(f"alter table trades rename to {trade_back_name}")) connection.execute(text(f"alter table trades rename to {trade_back_name}"))
@@ -160,8 +155,7 @@ def migrate_trades_and_orders_table(
max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag, max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag,
timeframe, open_trade_value, close_profit_abs, timeframe, open_trade_value, close_profit_abs,
trading_mode, leverage, liquidation_price, is_short, trading_mode, leverage, liquidation_price, is_short,
interest_rate, funding_fees, realized_profit, interest_rate, funding_fees
amount_precision, price_precision, precision_mode
) )
select id, lower(exchange), pair, {base_currency} base_currency, select id, lower(exchange), pair, {base_currency} base_currency,
{stake_currency} stake_currency, {stake_currency} stake_currency,
@@ -187,9 +181,7 @@ def migrate_trades_and_orders_table(
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
{trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price, {trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price,
{is_short} is_short, {interest_rate} interest_rate, {is_short} is_short, {interest_rate} interest_rate,
{funding_fees} funding_fees, {realized_profit} realized_profit, {funding_fees} funding_fees
{amount_precision} amount_precision, {price_precision} price_precision,
{precision_mode} precision_mode
from {trade_back_name} from {trade_back_name}
""")) """))
@@ -305,11 +297,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# Check if migration necessary # Check if migration necessary
# Migrates both trades and orders table! # Migrates both trades and orders table!
# if ('orders' not in previous_tables if not has_column(cols_orders, 'stop_price'):
# or not has_column(cols_orders, 'stop_price')): # if not has_column(cols_trades, 'base_currency'):
migrating = False
if not has_column(cols_trades, 'precision_mode'):
migrating = True
logger.info(f"Running database migration for trades - " logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}") f"backup: {table_back_name}, {order_table_bak_name}")
migrate_trades_and_orders_table( migrate_trades_and_orders_table(
@@ -317,7 +306,6 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
order_table_bak_name, cols_orders) order_table_bak_name, cols_orders)
if not has_column(cols_pairlocks, 'side'): if not has_column(cols_pairlocks, 'side'):
migrating = True
logger.info(f"Running database migration for pairlocks - " logger.info(f"Running database migration for pairlocks - "
f"backup: {pairlock_table_bak_name}") f"backup: {pairlock_table_bak_name}")
@@ -332,6 +320,3 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
set_sqlite_to_wal(engine) set_sqlite_to_wal(engine)
fix_old_dry_orders(engine) fix_old_dry_orders(engine)
if migrating:
logger.info("Database migration finished.")

View File

@@ -53,7 +53,7 @@ def init_db(db_url: str) -> None:
# https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope
# Scoped sessions proxy requests to the appropriate thread-local session. # Scoped sessions proxy requests to the appropriate thread-local session.
# We should use the scoped_session object - not a seperately initialized version # We should use the scoped_session object - not a seperately initialized version
Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=False)) Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=True))
Trade.query = Trade._session.query_property() Trade.query = Trade._session.query_property()
Order.query = Trade._session.query_property() Order.query = Trade._session.query_property()
PairLock.query = Trade._session.query_property() PairLock.query = Trade._session.query_property()
@@ -61,3 +61,11 @@ def init_db(db_url: str) -> None:
previous_tables = inspect(engine).get_table_names() previous_tables = inspect(engine).get_table_names()
_DECL_BASE.metadata.create_all(engine) _DECL_BASE.metadata.create_all(engine)
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables) check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
def cleanup_db() -> None:
"""
Flushes all pending operations to disk.
:return: None
"""
Trade.commit()

View File

@@ -3,21 +3,18 @@ This module contains the class to persist trades into SQLite
""" """
import logging import logging
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import isclose from decimal import Decimal
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String, from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
UniqueConstraint, desc, func) UniqueConstraint, desc, func)
from sqlalchemy.orm import Query, lazyload, relationship from sqlalchemy.orm import Query, lazyload, relationship
from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort
BuySell, LongShort)
from freqtrade.enums import ExitType, TradingMode from freqtrade.enums import ExitType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import amount_to_precision, price_to_precision
from freqtrade.leverage import interest from freqtrade.leverage import interest
from freqtrade.persistence.base import _DECL_BASE from freqtrade.persistence.base import _DECL_BASE
from freqtrade.util import FtPrecise
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -179,9 +176,10 @@ class Order(_DECL_BASE):
self.remaining = 0 self.remaining = 0
self.status = 'closed' self.status = 'closed'
self.ft_is_open = False self.ft_is_open = False
if (self.ft_order_side == trade.entry_side): if (self.ft_order_side == trade.entry_side
and len(trade.select_filled_orders(trade.entry_side)) == 1):
trade.open_rate = self.price trade.open_rate = self.price
trade.recalc_trade_from_orders() trade.recalc_open_trade_value()
trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True) trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True)
@staticmethod @staticmethod
@@ -197,7 +195,7 @@ class Order(_DECL_BASE):
if filtered_orders: if filtered_orders:
oobj = filtered_orders[0] oobj = filtered_orders[0]
oobj.update_from_ccxt_object(order) oobj.update_from_ccxt_object(order)
Trade.commit() Order.query.session.commit()
else: else:
logger.warning(f"Did not find order for {order}.") logger.warning(f"Did not find order for {order}.")
@@ -239,7 +237,6 @@ class LocalTrade():
trades: List['LocalTrade'] = [] trades: List['LocalTrade'] = []
trades_open: List['LocalTrade'] = [] trades_open: List['LocalTrade'] = []
total_profit: float = 0 total_profit: float = 0
realized_profit: float = 0
id: int = 0 id: int = 0
@@ -293,9 +290,6 @@ class LocalTrade():
timeframe: Optional[int] = None timeframe: Optional[int] = None
trading_mode: TradingMode = TradingMode.SPOT trading_mode: TradingMode = TradingMode.SPOT
amount_precision: Optional[float] = None
price_precision: Optional[float] = None
precision_mode: Optional[int] = None
# Leverage trading properties # Leverage trading properties
liquidation_price: Optional[float] = None liquidation_price: Optional[float] = None
@@ -308,16 +302,6 @@ class LocalTrade():
# Futures properties # Futures properties
funding_fees: Optional[float] = None funding_fees: Optional[float] = None
@property
def stoploss_or_liquidation(self) -> float:
if self.liquidation_price:
if self.is_short:
return min(self.stop_loss, self.liquidation_price)
else:
return max(self.stop_loss, self.liquidation_price)
return self.stop_loss
@property @property
def buy_tag(self) -> Optional[str]: def buy_tag(self) -> Optional[str]:
""" """
@@ -453,7 +437,6 @@ class LocalTrade():
if self.close_date else None), if self.close_date else None),
'close_timestamp': int(self.close_date.replace( 'close_timestamp': int(self.close_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
'realized_profit': self.realized_profit or 0.0,
'close_rate': self.close_rate, 'close_rate': self.close_rate,
'close_rate_requested': self.close_rate_requested, 'close_rate_requested': self.close_rate_requested,
'close_profit': self.close_profit, # Deprecated 'close_profit': self.close_profit, # Deprecated
@@ -514,7 +497,7 @@ class LocalTrade():
self.max_rate = max(current_price, self.max_rate or self.open_rate) self.max_rate = max(current_price, self.max_rate or self.open_rate)
self.min_rate = min(current_price_low, self.min_rate or self.open_rate) self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
def set_liquidation_price(self, liquidation_price: Optional[float]): def set_isolated_liq(self, liquidation_price: Optional[float]):
""" """
Method you should use to set self.liquidation price. Method you should use to set self.liquidation price.
Assures stop_loss is not passed the liquidation price Assures stop_loss is not passed the liquidation price
@@ -523,14 +506,22 @@ class LocalTrade():
return return
self.liquidation_price = liquidation_price self.liquidation_price = liquidation_price
def __set_stop_loss(self, stop_loss: float, percent: float): def _set_stop_loss(self, stop_loss: float, percent: float):
""" """
Method used internally to set self.stop_loss. Method you should use to set self.stop_loss.
Assures stop_loss is not passed the liquidation price
""" """
stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode) if self.liquidation_price is not None:
if self.is_short:
sl = min(stop_loss, self.liquidation_price)
else:
sl = max(stop_loss, self.liquidation_price)
else:
sl = stop_loss
if not self.stop_loss: if not self.stop_loss:
self.initial_stop_loss = stop_loss_norm self.initial_stop_loss = sl
self.stop_loss = stop_loss_norm self.stop_loss = sl
self.stop_loss_pct = -1 * abs(percent) self.stop_loss_pct = -1 * abs(percent)
self.stoploss_last_update = datetime.utcnow() self.stoploss_last_update = datetime.utcnow()
@@ -552,14 +543,19 @@ class LocalTrade():
leverage = self.leverage or 1.0 leverage = self.leverage or 1.0
if self.is_short: if self.is_short:
new_loss = float(current_price * (1 + abs(stoploss / leverage))) new_loss = float(current_price * (1 + abs(stoploss / leverage)))
# If trading with leverage, don't set the stoploss below the liquidation price
if self.liquidation_price:
new_loss = min(self.liquidation_price, new_loss)
else: else:
new_loss = float(current_price * (1 - abs(stoploss / leverage))) new_loss = float(current_price * (1 - abs(stoploss / leverage)))
# If trading with leverage, don't set the stoploss below the liquidation price
if self.liquidation_price:
new_loss = max(self.liquidation_price, new_loss)
# no stop loss assigned yet # no stop loss assigned yet
if self.initial_stop_loss_pct is None or refresh: if self.initial_stop_loss_pct is None or refresh:
self.__set_stop_loss(new_loss, stoploss) self._set_stop_loss(new_loss, stoploss)
self.initial_stop_loss = price_to_precision( self.initial_stop_loss = new_loss
new_loss, self.price_precision, self.precision_mode)
self.initial_stop_loss_pct = -1 * abs(stoploss) self.initial_stop_loss_pct = -1 * abs(stoploss)
# evaluate if the stop loss needs to be updated # evaluate if the stop loss needs to be updated
@@ -573,7 +569,7 @@ class LocalTrade():
# ? decreasing the minimum stoploss # ? decreasing the minimum stoploss
if (higher_stop and not self.is_short) or (lower_stop and self.is_short): if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
logger.debug(f"{self.pair} - Adjusting stoploss...") logger.debug(f"{self.pair} - Adjusting stoploss...")
self.__set_stop_loss(new_loss, stoploss) self._set_stop_loss(new_loss, stoploss)
else: else:
logger.debug(f"{self.pair} - Keeping current stoploss...") logger.debug(f"{self.pair} - Keeping current stoploss...")
@@ -605,29 +601,14 @@ class LocalTrade():
if self.is_open: if self.is_open:
payment = "SELL" if self.is_short else "BUY" payment = "SELL" if self.is_short else "BUY"
logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.') logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
# condition to avoid reset value when updating fees
if self.open_order_id == order.order_id:
self.open_order_id = None self.open_order_id = None
else:
logger.warning(
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
self.recalc_trade_from_orders() self.recalc_trade_from_orders()
elif order.ft_order_side == self.exit_side: elif order.ft_order_side == self.exit_side:
if self.is_open: if self.is_open:
payment = "BUY" if self.is_short else "SELL" payment = "BUY" if self.is_short else "SELL"
# * On margin shorts, you buy a little bit more than the amount (amount + interest) # * On margin shorts, you buy a little bit more than the amount (amount + interest)
logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.') logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
# condition to avoid reset value when updating fees
if self.open_order_id == order.order_id:
self.open_order_id = None
else:
logger.warning(
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
amount_tr = amount_to_precision(self.amount, self.amount_precision, self.precision_mode)
if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC):
self.close(order.safe_price) self.close(order.safe_price)
else:
self.recalc_trade_from_orders()
elif order.ft_order_side == 'stoploss': elif order.ft_order_side == 'stoploss':
self.stoploss_order_id = None self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss self.close_rate_requested = self.stop_loss
@@ -646,11 +627,11 @@ class LocalTrade():
""" """
self.close_rate = rate self.close_rate = rate
self.close_date = self.close_date or datetime.utcnow() self.close_date = self.close_date or datetime.utcnow()
self.close_profit_abs = self.calc_profit(rate) + self.realized_profit self.close_profit = self.calc_profit_ratio(rate)
self.close_profit_abs = self.calc_profit(rate)
self.is_open = False self.is_open = False
self.exit_order_status = 'closed' self.exit_order_status = 'closed'
self.open_order_id = None self.open_order_id = None
self.recalc_trade_from_orders(is_closing=True)
if show_msg: if show_msg:
logger.info( logger.info(
'Marking %s as closed as the trade is fulfilled and found no open orders for it.', 'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
@@ -696,13 +677,13 @@ class LocalTrade():
""" """
return len([o for o in self.orders if o.ft_order_side == self.exit_side]) return len([o for o in self.orders if o.ft_order_side == self.exit_side])
def _calc_open_trade_value(self, amount: float, open_rate: float) -> float: def _calc_open_trade_value(self) -> float:
""" """
Calculate the open_rate including open_fee. Calculate the open_rate including open_fee.
:return: Price in of the open trade incl. Fees :return: Price in of the open trade incl. Fees
""" """
open_trade = FtPrecise(amount) * FtPrecise(open_rate) open_trade = Decimal(self.amount) * Decimal(self.open_rate)
fees = open_trade * FtPrecise(self.fee_open) fees = open_trade * Decimal(self.fee_open)
if self.is_short: if self.is_short:
return float(open_trade - fees) return float(open_trade - fees)
else: else:
@@ -713,39 +694,39 @@ class LocalTrade():
Recalculate open_trade_value. Recalculate open_trade_value.
Must be called whenever open_rate, fee_open is changed. Must be called whenever open_rate, fee_open is changed.
""" """
self.open_trade_value = self._calc_open_trade_value(self.amount, self.open_rate) self.open_trade_value = self._calc_open_trade_value()
def calculate_interest(self) -> FtPrecise: def calculate_interest(self) -> Decimal:
""" """
Calculate interest for this trade. Only applicable for Margin trading. Calculate interest for this trade. Only applicable for Margin trading.
""" """
zero = FtPrecise(0.0) zero = Decimal(0.0)
# If nothing was borrowed # If nothing was borrowed
if self.trading_mode != TradingMode.MARGIN or self.has_no_leverage: if self.trading_mode != TradingMode.MARGIN or self.has_no_leverage:
return zero return zero
open_date = self.open_date.replace(tzinfo=None) open_date = self.open_date.replace(tzinfo=None)
now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None) now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
sec_per_hour = FtPrecise(3600) sec_per_hour = Decimal(3600)
total_seconds = FtPrecise((now - open_date).total_seconds()) total_seconds = Decimal((now - open_date).total_seconds())
hours = total_seconds / sec_per_hour or zero hours = total_seconds / sec_per_hour or zero
rate = FtPrecise(self.interest_rate) rate = Decimal(self.interest_rate)
borrowed = FtPrecise(self.borrowed) borrowed = Decimal(self.borrowed)
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours) return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
def _calc_base_close(self, amount: FtPrecise, rate: float, fee: float) -> FtPrecise: def _calc_base_close(self, amount: Decimal, rate: float, fee: float) -> Decimal:
close_trade = amount * FtPrecise(rate) close_trade = amount * Decimal(rate)
fees = close_trade * FtPrecise(fee) fees = close_trade * Decimal(fee)
if self.is_short: if self.is_short:
return close_trade + fees return close_trade + fees
else: else:
return close_trade - fees return close_trade - fees
def calc_close_trade_value(self, rate: float, amount: float = None) -> float: def calc_close_trade_value(self, rate: float) -> float:
""" """
Calculate the Trade's close value including fees Calculate the Trade's close value including fees
:param rate: rate to compare with. :param rate: rate to compare with.
@@ -754,146 +735,96 @@ class LocalTrade():
if rate is None and not self.close_rate: if rate is None and not self.close_rate:
return 0.0 return 0.0
amount1 = FtPrecise(amount or self.amount) amount = Decimal(self.amount)
trading_mode = self.trading_mode or TradingMode.SPOT trading_mode = self.trading_mode or TradingMode.SPOT
if trading_mode == TradingMode.SPOT: if trading_mode == TradingMode.SPOT:
return float(self._calc_base_close(amount1, rate, self.fee_close)) return float(self._calc_base_close(amount, rate, self.fee_close))
elif (trading_mode == TradingMode.MARGIN): elif (trading_mode == TradingMode.MARGIN):
total_interest = self.calculate_interest() total_interest = self.calculate_interest()
if self.is_short: if self.is_short:
amount1 = amount1 + total_interest amount = amount + total_interest
return float(self._calc_base_close(amount1, rate, self.fee_close)) return float(self._calc_base_close(amount, rate, self.fee_close))
else: else:
# Currency already owned for longs, no need to purchase # Currency already owned for longs, no need to purchase
return float(self._calc_base_close(amount1, rate, self.fee_close) - total_interest) return float(self._calc_base_close(amount, rate, self.fee_close) - total_interest)
elif (trading_mode == TradingMode.FUTURES): elif (trading_mode == TradingMode.FUTURES):
funding_fees = self.funding_fees or 0.0 funding_fees = self.funding_fees or 0.0
# Positive funding_fees -> Trade has gained from fees. # Positive funding_fees -> Trade has gained from fees.
# Negative funding_fees -> Trade had to pay the fees. # Negative funding_fees -> Trade had to pay the fees.
if self.is_short: if self.is_short:
return float(self._calc_base_close(amount1, rate, self.fee_close)) - funding_fees return float(self._calc_base_close(amount, rate, self.fee_close)) - funding_fees
else: else:
return float(self._calc_base_close(amount1, rate, self.fee_close)) + funding_fees return float(self._calc_base_close(amount, rate, self.fee_close)) + funding_fees
else: else:
raise OperationalException( raise OperationalException(
f"{self.trading_mode.value} trading is not yet available using freqtrade") f"{self.trading_mode.value} trading is not yet available using freqtrade")
def calc_profit(self, rate: float, amount: float = None, open_rate: float = None) -> float: def calc_profit(self, rate: float) -> float:
""" """
Calculate the absolute profit in stake currency between Close and Open trade Calculate the absolute profit in stake currency between Close and Open trade
:param rate: close rate to compare with. :param rate: close rate to compare with.
:param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
:param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
:return: profit in stake currency as float :return: profit in stake currency as float
""" """
close_trade_value = self.calc_close_trade_value(rate, amount) close_trade_value = self.calc_close_trade_value(rate)
if amount is None or open_rate is None:
open_trade_value = self.open_trade_value
else:
open_trade_value = self._calc_open_trade_value(amount, open_rate)
if self.is_short: if self.is_short:
profit = open_trade_value - close_trade_value profit = self.open_trade_value - close_trade_value
else: else:
profit = close_trade_value - open_trade_value profit = close_trade_value - self.open_trade_value
return float(f"{profit:.8f}") return float(f"{profit:.8f}")
def calc_profit_ratio( def calc_profit_ratio(self, rate: float) -> float:
self, rate: float, amount: float = None, open_rate: float = None) -> float:
""" """
Calculates the profit as ratio (including fee). Calculates the profit as ratio (including fee).
:param rate: rate to compare with. :param rate: rate to compare with.
:param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
:param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
:return: profit ratio as float :return: profit ratio as float
""" """
close_trade_value = self.calc_close_trade_value(rate, amount) close_trade_value = self.calc_close_trade_value(rate)
if amount is None or open_rate is None:
open_trade_value = self.open_trade_value
else:
open_trade_value = self._calc_open_trade_value(amount, open_rate)
short_close_zero = (self.is_short and close_trade_value == 0.0) short_close_zero = (self.is_short and close_trade_value == 0.0)
long_close_zero = (not self.is_short and open_trade_value == 0.0) long_close_zero = (not self.is_short and self.open_trade_value == 0.0)
leverage = self.leverage or 1.0 leverage = self.leverage or 1.0
if (short_close_zero or long_close_zero): if (short_close_zero or long_close_zero):
return 0.0 return 0.0
else: else:
if self.is_short: if self.is_short:
profit_ratio = (1 - (close_trade_value / open_trade_value)) * leverage profit_ratio = (1 - (close_trade_value / self.open_trade_value)) * leverage
else: else:
profit_ratio = ((close_trade_value / open_trade_value) - 1) * leverage profit_ratio = ((close_trade_value / self.open_trade_value) - 1) * leverage
return float(f"{profit_ratio:.8f}") return float(f"{profit_ratio:.8f}")
def recalc_trade_from_orders(self, *, is_closing: bool = False): def recalc_trade_from_orders(self):
ZERO = FtPrecise(0.0)
current_amount = FtPrecise(0.0)
current_stake = FtPrecise(0.0)
total_stake = 0.0 # Total stake after all buy orders (does not subtract!)
avg_price = FtPrecise(0.0)
close_profit = 0.0
close_profit_abs = 0.0
total_amount = 0.0
total_stake = 0.0
for o in self.orders: for o in self.orders:
if o.ft_is_open or not o.filled: if (o.ft_is_open or
(o.ft_order_side != self.entry_side) or
(o.status not in NON_OPEN_EXCHANGE_STATES)):
continue continue
tmp_amount = FtPrecise(o.safe_amount_after_fee) tmp_amount = o.safe_amount_after_fee
tmp_price = FtPrecise(o.safe_price) tmp_price = o.average or o.price
if tmp_amount > 0.0 and tmp_price is not None:
total_amount += tmp_amount
total_stake += tmp_price * tmp_amount
is_exit = o.ft_order_side != self.entry_side if total_amount > 0:
side = FtPrecise(-1 if is_exit else 1)
if tmp_amount > ZERO and tmp_price is not None:
current_amount += tmp_amount * side
price = avg_price if is_exit else tmp_price
current_stake += price * tmp_amount * side
if current_amount > ZERO:
avg_price = current_stake / current_amount
if is_exit:
# Process partial exits
exit_rate = o.safe_price
exit_amount = o.safe_amount_after_fee
profit = self.calc_profit(rate=exit_rate, amount=exit_amount,
open_rate=float(avg_price))
close_profit_abs += profit
close_profit = self.calc_profit_ratio(
exit_rate, amount=exit_amount, open_rate=avg_price)
if current_amount <= ZERO:
profit = close_profit_abs
else:
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
if close_profit:
self.close_profit = close_profit
self.realized_profit = close_profit_abs
self.close_profit_abs = profit
current_amount_tr = amount_to_precision(float(current_amount),
self.amount_precision, self.precision_mode)
if current_amount_tr > 0.0:
# Trade is still open
# Leverage not updated, as we don't allow changing leverage through DCA at the moment. # Leverage not updated, as we don't allow changing leverage through DCA at the moment.
self.open_rate = float(current_stake / current_amount) self.open_rate = total_stake / total_amount
self.amount = current_amount_tr self.stake_amount = total_stake / (self.leverage or 1.0)
self.stake_amount = float(current_stake) / (self.leverage or 1.0) self.amount = total_amount
self.fee_open_cost = self.fee_open * float(current_stake) self.fee_open_cost = self.fee_open * total_stake
self.recalc_open_trade_value() self.recalc_open_trade_value()
if self.stop_loss_pct is not None and self.open_rate is not None: if self.stop_loss_pct is not None and self.open_rate is not None:
self.adjust_stop_loss(self.open_rate, self.stop_loss_pct) self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)
elif is_closing and total_stake > 0:
# Close profit abs / maximum owned
# Fees are considered as they are part of close_profit_abs
self.close_profit = (close_profit_abs / total_stake) * self.leverage
def select_order_by_order_id(self, order_id: str) -> Optional[Order]: def select_order_by_order_id(self, order_id: str) -> Optional[Order]:
""" """
@@ -915,7 +846,7 @@ class LocalTrade():
""" """
orders = self.orders orders = self.orders
if order_side: if order_side:
orders = [o for o in orders if o.ft_order_side == order_side] orders = [o for o in self.orders if o.ft_order_side == order_side]
if is_open is not None: if is_open is not None:
orders = [o for o in orders if o.ft_is_open == is_open] orders = [o for o in orders if o.ft_is_open == is_open]
if len(orders) > 0: if len(orders) > 0:
@@ -930,9 +861,9 @@ class LocalTrade():
:return: array of Order objects :return: array of Order objects
""" """
return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None)) return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None))
and o.ft_is_open is False and o.ft_is_open is False and
and o.filled (o.filled or 0) > 0 and
and o.status in NON_OPEN_EXCHANGE_STATES] o.status in NON_OPEN_EXCHANGE_STATES]
def select_filled_or_open_orders(self) -> List['Order']: def select_filled_or_open_orders(self) -> List['Order']:
""" """
@@ -1097,7 +1028,6 @@ class Trade(_DECL_BASE, LocalTrade):
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)
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)
@@ -1129,9 +1059,6 @@ 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)
price_precision = Column(Float, nullable=True)
precision_mode = Column(Integer, nullable=True)
# Leverage trading properties # Leverage trading properties
leverage = Column(Float, nullable=True, default=1.0) leverage = Column(Float, nullable=True, default=1.0)
@@ -1146,7 +1073,6 @@ class Trade(_DECL_BASE, LocalTrade):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.realized_profit = 0
self.recalc_open_trade_value() self.recalc_open_trade_value()
def delete(self) -> None: def delete(self) -> None:
@@ -1161,10 +1087,6 @@ class Trade(_DECL_BASE, LocalTrade):
def commit(): def commit():
Trade.query.session.commit() Trade.query.session.commit()
@staticmethod
def rollback():
Trade.query.session.rollback()
@staticmethod @staticmethod
def get_trades_proxy(*, pair: str = None, is_open: bool = None, def get_trades_proxy(*, pair: str = None, is_open: bool = None,
open_date: datetime = None, close_date: datetime = None, open_date: datetime = None, close_date: datetime = None,
@@ -1317,7 +1239,7 @@ class Trade(_DECL_BASE, LocalTrade):
""" """
filters = [Trade.is_open.is_(False)] filters = [Trade.is_open.is_(False)]
if (pair is not None): if(pair is not None):
filters.append(Trade.pair == pair) filters.append(Trade.pair == pair)
enter_tag_perf = Trade.query.with_entities( enter_tag_perf = Trade.query.with_entities(
@@ -1350,7 +1272,7 @@ class Trade(_DECL_BASE, LocalTrade):
""" """
filters = [Trade.is_open.is_(False)] filters = [Trade.is_open.is_(False)]
if (pair is not None): if(pair is not None):
filters.append(Trade.pair == pair) filters.append(Trade.pair == pair)
sell_tag_perf = Trade.query.with_entities( sell_tag_perf = Trade.query.with_entities(
@@ -1383,7 +1305,7 @@ class Trade(_DECL_BASE, LocalTrade):
""" """
filters = [Trade.is_open.is_(False)] filters = [Trade.is_open.is_(False)]
if (pair is not None): if(pair is not None):
filters.append(Trade.pair == pair) filters.append(Trade.pair == pair)
mix_tag_perf = Trade.query.with_entities( mix_tag_perf = Trade.query.with_entities(
@@ -1403,7 +1325,7 @@ class Trade(_DECL_BASE, LocalTrade):
enter_tag = enter_tag if enter_tag is not None else "Other" enter_tag = enter_tag if enter_tag is not None else "Other"
exit_reason = exit_reason if exit_reason is not None else "Other" exit_reason = exit_reason if exit_reason is not None else "Other"
if (exit_reason is not None and enter_tag is not None): if(exit_reason is not None and enter_tag is not None):
mix_tag = enter_tag + " " + exit_reason mix_tag = enter_tag + " " + exit_reason
i = 0 i = 0
if not any(item["mix_tag"] == mix_tag for item in return_list): if not any(item["mix_tag"] == mix_tag for item in return_list):

View File

@@ -8,11 +8,11 @@ from typing import Any, Dict, List, Optional
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade.configuration import PeriodicCache
from freqtrade.constants import ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural from freqtrade.misc import plural
from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.plugins.pairlist.IPairList import IPairList
from freqtrade.util import PeriodicCache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -51,11 +51,6 @@ class PrecisionFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.fetch_tickers() :param ticker: ticker dict as returned from ccxt.fetch_tickers()
:return: True if the pair can stay, false if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
if ticker.get('last', None) is None:
self.log_once(f"Removed {ticker['symbol']} from whitelist, because "
"ticker['last'] is empty (Usually no trade in the last 24h).",
logger.info)
return False
stop_price = ticker['last'] * self._stoploss stop_price = ticker['last'] * self._stoploss
# Adjust stop-prices to precision # Adjust stop-prices to precision

View File

@@ -4,14 +4,14 @@ Volume PairList provider
Provides dynamic pair list based on trade volumes Provides dynamic pair list based on trade volumes
""" """
import logging import logging
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List from typing import Any, Dict, List
import arrow
from cachetools import TTLCache from cachetools import TTLCache
from freqtrade.constants import ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import format_ms_time from freqtrade.misc import format_ms_time
from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.plugins.pairlist.IPairList import IPairList
@@ -158,16 +158,16 @@ class VolumePairList(IPairList):
filtered_tickers: List[Dict[str, Any]] = [{'symbol': k} for k in pairlist] filtered_tickers: List[Dict[str, Any]] = [{'symbol': k} for k in pairlist]
# get lookback period in ms, for exchange ohlcv fetch # get lookback period in ms, for exchange ohlcv fetch
since_ms = int(timeframe_to_prev_date( since_ms = int(arrow.utcnow()
self._lookback_timeframe, .floor('minute')
datetime.now(timezone.utc) + timedelta( .shift(minutes=-(self._lookback_period * self._tf_in_min)
minutes=-(self._lookback_period * self._tf_in_min) - self._tf_in_min) - self._tf_in_min)
).timestamp()) * 1000 .int_timestamp) * 1000
to_ms = int(timeframe_to_prev_date( to_ms = int(arrow.utcnow()
self._lookback_timeframe, .floor('minute')
datetime.now(timezone.utc) - timedelta(minutes=self._tf_in_min) .shift(minutes=-self._tf_in_min)
).timestamp()) * 1000 .int_timestamp) * 1000
# todo: utc date output for starting date # todo: utc date output for starting date
self.log_once(f"Using volume range of {self._lookback_period} candles, timeframe: " self.log_once(f"Using volume range of {self._lookback_period} candles, timeframe: "

View File

@@ -1,5 +1,5 @@
import re import re
from typing import Any, Dict, List from typing import List
def expand_pairlist(wildcardpl: List[str], available_pairs: List[str], def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
@@ -40,13 +40,3 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
except re.error as err: except re.error as err:
raise ValueError(f"Wildcard error in {pair_wc}, {err}") raise ValueError(f"Wildcard error in {pair_wc}, {err}")
return result return result
def dynamic_expand_pairlist(config: Dict[str, Any], markets: List[str]) -> List[str]:
expanded_pairs = expand_pairlist(config['pairs'], markets)
if config.get('freqai', {}).get('enabled', False):
corr_pairlist = config['freqai']['feature_parameters']['include_corr_pairlist']
expanded_pairs += [pair for pair in corr_pairlist
if pair not in config['pairs']]
return expanded_pairs

View File

@@ -49,7 +49,7 @@ class StoplossGuard(IProtection):
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
trades = [trade for trade in trades1 if (str(trade.exit_reason) in ( trades = [trade for trade in trades1 if (str(trade.exit_reason) in (
ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value, ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value,
ExitType.STOPLOSS_ON_EXCHANGE.value, ExitType.LIQUIDATION.value) ExitType.STOPLOSS_ON_EXCHANGE.value)
and trade.close_profit and trade.close_profit < self._profit_limit)] and trade.close_profit and trade.close_profit < self._profit_limit)]
if self._only_per_side: if self._only_per_side:

View File

@@ -1,57 +0,0 @@
# pragma pylint: disable=attribute-defined-outside-init
"""
This module load a custom model for freqai
"""
import logging
from pathlib import Path
from typing import Dict
from freqtrade.constants import USERPATH_FREQAIMODELS
from freqtrade.exceptions import OperationalException
from freqtrade.freqai.freqai_interface import IFreqaiModel
from freqtrade.resolvers import IResolver
logger = logging.getLogger(__name__)
class FreqaiModelResolver(IResolver):
"""
This class contains all the logic to load custom hyperopt loss class
"""
object_type = IFreqaiModel
object_type_str = "FreqaiModel"
user_subdir = USERPATH_FREQAIMODELS
initial_search_path = (
Path(__file__).parent.parent.joinpath("freqai/prediction_models").resolve()
)
@staticmethod
def load_freqaimodel(config: Dict) -> IFreqaiModel:
"""
Load the custom class from config parameter
:param config: configuration dictionary
"""
disallowed_models = ["BaseRegressionModel", "BaseTensorFlowModel"]
freqaimodel_name = config.get("freqaimodel")
if not freqaimodel_name:
raise OperationalException(
"No freqaimodel set. Please use `--freqaimodel` to "
"specify the FreqaiModel class to use.\n"
)
if freqaimodel_name in disallowed_models:
raise OperationalException(
f"{freqaimodel_name} is a baseclass and cannot be used directly. Please choose "
"an existing child class or inherit from this baseclass.\n"
)
freqaimodel = FreqaiModelResolver.load_object(
freqaimodel_name,
config,
kwargs={"config": config},
extra_dir=config.get("freqaimodel_path"),
)
return freqaimodel

View File

@@ -194,11 +194,11 @@ class OrderSchema(BaseModel):
pair: str pair: str
order_id: str order_id: str
status: str status: str
remaining: Optional[float] remaining: float
amount: float amount: float
safe_price: float safe_price: float
cost: float cost: float
filled: Optional[float] filled: float
ft_order_side: str ft_order_side: str
order_type: str order_type: str
is_open: bool is_open: bool
@@ -325,13 +325,11 @@ class ForceEnterPayload(BaseModel):
ordertype: Optional[OrderTypeValues] ordertype: Optional[OrderTypeValues]
stakeamount: Optional[float] stakeamount: Optional[float]
entry_tag: Optional[str] entry_tag: Optional[str]
leverage: Optional[float]
class ForceExitPayload(BaseModel): class ForceExitPayload(BaseModel):
tradeid: str tradeid: str
ordertype: Optional[OrderTypeValues] ordertype: Optional[OrderTypeValues]
amount: Optional[float]
class BlacklistPayload(BaseModel): class BlacklistPayload(BaseModel):

View File

@@ -37,8 +37,7 @@ logger = logging.getLogger(__name__)
# 2.14: Add entry/exit orders to trade response # 2.14: Add entry/exit orders to trade response
# 2.15: Add backtest history endpoints # 2.15: Add backtest history endpoints
# 2.16: Additional daily metrics # 2.16: Additional daily metrics
# 2.17: Forceentry - leverage, partial force_exit API_VERSION = 2.16
API_VERSION = 2.17
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() router_public = APIRouter()
@@ -143,11 +142,12 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
@router.post('/forcebuy', response_model=ForceEnterResponse, tags=['trading']) @router.post('/forcebuy', response_model=ForceEnterResponse, tags=['trading'])
def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None ordertype = payload.ordertype.value if payload.ordertype else None
stake_amount = payload.stakeamount if payload.stakeamount else None
entry_tag = payload.entry_tag if payload.entry_tag else 'force_entry'
trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side, trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
order_type=ordertype, stake_amount=payload.stakeamount, order_type=ordertype, stake_amount=stake_amount,
enter_tag=payload.entry_tag or 'force_entry', enter_tag=entry_tag)
leverage=payload.leverage)
if trade: if trade:
return ForceEnterResponse.parse_obj(trade.to_json()) return ForceEnterResponse.parse_obj(trade.to_json())
@@ -161,7 +161,7 @@ def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
@router.post('/forcesell', response_model=ResultMsg, tags=['trading']) @router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)): def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None ordertype = payload.ordertype.value if payload.ordertype else None
return rpc._rpc_force_exit(payload.tradeid, ordertype, amount=payload.amount) return rpc._rpc_force_exit(payload.tradeid, ordertype)
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) @router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])

View File

@@ -18,9 +18,9 @@ def get_rpc_optional() -> Optional[RPC]:
def get_rpc() -> Optional[Iterator[RPC]]: def get_rpc() -> Optional[Iterator[RPC]]:
_rpc = get_rpc_optional() _rpc = get_rpc_optional()
if _rpc: if _rpc:
Trade.rollback() Trade.query.session.rollback()
yield _rpc yield _rpc
Trade.rollback() Trade.query.session.rollback()
else: else:
raise RPCException('Bot is not in the correct state') raise RPCException('Bot is not in the correct state')

View File

@@ -1,5 +1,4 @@
from pathlib import Path from pathlib import Path
from typing import Optional
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
@@ -51,12 +50,8 @@ async def index_html(rest_of_path: str):
filename = uibase / rest_of_path filename = uibase / rest_of_path
# It's security relevant to check "relative_to". # It's security relevant to check "relative_to".
# Without this, Directory-traversal is possible. # Without this, Directory-traversal is possible.
media_type: Optional[str] = None
if filename.suffix == '.js':
# Force text/javascript for .js files - Circumvent faulty system configuration
media_type = 'application/javascript'
if filename.is_file() and is_relative_to(filename, uibase): if filename.is_file() and is_relative_to(filename, uibase):
return FileResponse(str(filename), media_type=media_type) return FileResponse(str(filename))
index_file = uibase / 'index.html' index_file = uibase / 'index.html'
if not index_file.is_file(): if not index_file.is_file():

View File

@@ -12,7 +12,6 @@ from pycoingecko import CoinGeckoAPI
from requests.exceptions import RequestException from requests.exceptions import RequestException
from freqtrade.constants import SUPPORTED_FIAT from freqtrade.constants import SUPPORTED_FIAT
from freqtrade.mixins.logging_mixin import LoggingMixin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -28,7 +27,7 @@ coingecko_mapping = {
} }
class CryptoToFiatConverter(LoggingMixin): class CryptoToFiatConverter:
""" """
Main class to initiate Crypto to FIAT. Main class to initiate Crypto to FIAT.
This object contains a list of pair Crypto, FIAT This object contains a list of pair Crypto, FIAT
@@ -55,7 +54,6 @@ class CryptoToFiatConverter(LoggingMixin):
# Timeout: 6h # Timeout: 6h
self._pair_price: TTLCache = TTLCache(maxsize=500, ttl=6 * 60 * 60) self._pair_price: TTLCache = TTLCache(maxsize=500, ttl=6 * 60 * 60)
LoggingMixin.__init__(self, logger, 3600)
self._load_cryptomap() self._load_cryptomap()
def _load_cryptomap(self) -> None: def _load_cryptomap(self) -> None:
@@ -179,9 +177,7 @@ class CryptoToFiatConverter(LoggingMixin):
if not _gekko_id: if not _gekko_id:
# return 0 for unsupported stake currencies (fiat-convert should not break the bot) # return 0 for unsupported stake currencies (fiat-convert should not break the bot)
self.log_once( logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
f"unsupported crypto-symbol {crypto_symbol.upper()} - returning 0.0",
logger.warning)
return 0.0 return 0.0
try: try:

View File

@@ -179,10 +179,8 @@ class RPC:
else: else:
current_rate = trade.close_rate current_rate = trade.close_rate
if len(trade.select_filled_orders(trade.entry_side)) > 0: if len(trade.select_filled_orders(trade.entry_side)) > 0:
current_profit = trade.calc_profit_ratio( current_profit = trade.calc_profit_ratio(current_rate)
current_rate) if not isnan(current_rate) else NAN current_profit_abs = trade.calc_profit(current_rate)
current_profit_abs = trade.calc_profit(
current_rate) if not isnan(current_rate) else NAN
current_profit_fiat: Optional[float] = None current_profit_fiat: Optional[float] = None
# Calculate fiat profit # Calculate fiat profit
if self._fiat_converter: if self._fiat_converter:
@@ -203,7 +201,7 @@ class RPC:
trade_dict = trade.to_json() trade_dict = trade.to_json()
trade_dict.update(dict( trade_dict.update(dict(
close_profit=trade.close_profit if not trade.is_open else None, close_profit=trade.close_profit if trade.close_profit is not None else None,
current_rate=current_rate, current_rate=current_rate,
current_profit=current_profit, # Deprecated current_profit=current_profit, # Deprecated
current_profit_pct=round(current_profit * 100, 2), # Deprecated current_profit_pct=round(current_profit * 100, 2), # Deprecated
@@ -241,10 +239,7 @@ class RPC:
trade.pair, side='exit', is_short=trade.is_short, refresh=False) trade.pair, side='exit', is_short=trade.is_short, refresh=False)
except (PricingError, ExchangeError): except (PricingError, ExchangeError):
current_rate = NAN current_rate = NAN
trade_profit = NAN if len(trade.select_filled_orders(trade.entry_side)) > 0:
profit_str = f'{NAN:.2%}'
else:
if trade.nr_of_successful_entries > 0:
trade_profit = trade.calc_profit(current_rate) trade_profit = trade.calc_profit(current_rate)
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}' profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
else: else:
@@ -429,20 +424,21 @@ class RPC:
for trade in trades: for trade in trades:
current_rate: float = 0.0 current_rate: float = 0.0
if not trade.open_rate:
continue
if trade.close_date: if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds()) durations.append((trade.close_date - trade.open_date).total_seconds())
if not trade.is_open: if not trade.is_open:
profit_ratio = trade.close_profit profit_ratio = trade.close_profit
profit_abs = trade.close_profit_abs profit_closed_coin.append(trade.close_profit_abs)
profit_closed_coin.append(profit_abs)
profit_closed_ratio.append(profit_ratio) profit_closed_ratio.append(profit_ratio)
if trade.close_profit >= 0: if trade.close_profit >= 0:
winning_trades += 1 winning_trades += 1
winning_profit += profit_abs winning_profit += trade.close_profit_abs
else: else:
losing_trades += 1 losing_trades += 1
losing_profit += profit_abs losing_profit += trade.close_profit_abs
else: else:
# Get current rate # Get current rate
try: try:
@@ -450,15 +446,11 @@ class RPC:
trade.pair, side='exit', is_short=trade.is_short, refresh=False) trade.pair, side='exit', is_short=trade.is_short, refresh=False)
except (PricingError, ExchangeError): except (PricingError, ExchangeError):
current_rate = NAN current_rate = NAN
if isnan(current_rate):
profit_ratio = NAN
profit_abs = NAN
else:
profit_ratio = trade.calc_profit_ratio(rate=current_rate) profit_ratio = trade.calc_profit_ratio(rate=current_rate)
profit_abs = trade.calc_profit(
rate=trade.close_rate or current_rate) + trade.realized_profit
profit_all_coin.append(profit_abs) profit_all_coin.append(
trade.calc_profit(rate=trade.close_rate or current_rate)
)
profit_all_ratio.append(profit_ratio) profit_all_ratio.append(profit_ratio)
best_pair = Trade.get_best_pair(start_date) best_pair = Trade.get_best_pair(start_date)
@@ -667,8 +659,12 @@ class RPC:
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'} return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
def __exec_force_exit(self, trade: Trade, ordertype: Optional[str], def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
amount: Optional[float] = None) -> None: """
Handler for forceexit <id>.
Sells the given trade at current price
"""
def _exec_force_exit(trade: Trade) -> None:
# Check if there is there is an open order # Check if there is there is an open order
fully_canceled = False fully_canceled = False
if trade.open_order_id: if trade.open_order_id:
@@ -689,26 +685,10 @@ class RPC:
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT) exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
order_type = ordertype or self._freqtrade.strategy.order_types.get( order_type = ordertype or self._freqtrade.strategy.order_types.get(
"force_exit", self._freqtrade.strategy.order_types["exit"]) "force_exit", self._freqtrade.strategy.order_types["exit"])
sub_amount: Optional[float] = None
if amount and amount < trade.amount:
# Partial exit ...
min_exit_stake = self._freqtrade.exchange.get_min_pair_stake_amount(
trade.pair, current_rate, trade.stop_loss_pct)
remaining = (trade.amount - amount) * current_rate
if remaining < min_exit_stake:
raise RPCException(f'Remaining amount of {remaining} would be too small.')
sub_amount = amount
self._freqtrade.execute_trade_exit( self._freqtrade.execute_trade_exit(
trade, current_rate, exit_check, ordertype=order_type, trade, current_rate, exit_check, ordertype=order_type)
sub_trade_amt=sub_amount) # ---- EOF def _exec_forcesell ----
def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None, *,
amount: Optional[float] = None) -> Dict[str, str]:
"""
Handler for forceexit <id>.
Sells the given trade at current price
"""
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running') raise RPCException('trader is not running')
@@ -717,7 +697,7 @@ class RPC:
if trade_id == 'all': if trade_id == 'all':
# Execute sell for all open orders # Execute sell for all open orders
for trade in Trade.get_open_trades(): for trade in Trade.get_open_trades():
self.__exec_force_exit(trade, ordertype) _exec_force_exit(trade)
Trade.commit() Trade.commit()
self._freqtrade.wallets.update() self._freqtrade.wallets.update()
return {'result': 'Created sell orders for all open trades.'} return {'result': 'Created sell orders for all open trades.'}
@@ -730,7 +710,7 @@ class RPC:
logger.warning('force_exit: Invalid argument received') logger.warning('force_exit: Invalid argument received')
raise RPCException('invalid argument') raise RPCException('invalid argument')
self.__exec_force_exit(trade, ordertype, amount) _exec_force_exit(trade)
Trade.commit() Trade.commit()
self._freqtrade.wallets.update() self._freqtrade.wallets.update()
return {'result': f'Created sell order for trade {trade_id}.'} return {'result': f'Created sell order for trade {trade_id}.'}
@@ -739,8 +719,7 @@ class RPC:
order_type: Optional[str] = None, order_type: Optional[str] = None,
order_side: SignalDirection = SignalDirection.LONG, order_side: SignalDirection = SignalDirection.LONG,
stake_amount: Optional[float] = None, stake_amount: Optional[float] = None,
enter_tag: Optional[str] = 'force_entry', enter_tag: Optional[str] = 'force_entry') -> Optional[Trade]:
leverage: Optional[float] = None) -> Optional[Trade]:
""" """
Handler for forcebuy <asset> <price> Handler for forcebuy <asset> <price>
Buys a pair trade at the given or current price Buys a pair trade at the given or current price
@@ -782,7 +761,6 @@ class RPC:
ordertype=order_type, trade=trade, ordertype=order_type, trade=trade,
is_short=is_short, is_short=is_short,
enter_tag=enter_tag, enter_tag=enter_tag,
leverage_=leverage,
): ):
Trade.commit() Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
@@ -897,7 +875,7 @@ class RPC:
lock.active = False lock.active = False
lock.lock_end_time = datetime.now(timezone.utc) lock.lock_end_time = datetime.now(timezone.utc)
Trade.commit() PairLock.query.session.commit()
return self._rpc_locks() return self._rpc_locks()

View File

@@ -2,7 +2,6 @@
This module contains class to manage RPC communications (Telegram, API, ...) This module contains class to manage RPC communications (Telegram, API, ...)
""" """
import logging import logging
from collections import deque
from typing import Any, Dict, List from typing import Any, Dict, List
from freqtrade.enums import RPCMessageType from freqtrade.enums import RPCMessageType
@@ -78,17 +77,6 @@ class RPCManager:
except NotImplementedError: except NotImplementedError:
logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.") logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.")
def process_msg_queue(self, queue: deque) -> None:
"""
Process all messages in the queue.
"""
while queue:
msg = queue.popleft()
self.send_msg({
'type': RPCMessageType.STRATEGY_MSG,
'msg': msg,
})
def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None: def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None:
if config['dry_run']: if config['dry_run']:
self.send_msg({ self.send_msg({

View File

@@ -16,8 +16,8 @@ from typing import Any, Callable, Dict, List, Optional, Union
import arrow import arrow
from tabulate import tabulate from tabulate import tabulate
from telegram import (MAX_MESSAGE_LENGTH, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton,
KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update) ParseMode, ReplyKeyboardMarkup, Update)
from telegram.error import BadRequest, NetworkError, TelegramError from telegram.error import BadRequest, NetworkError, TelegramError
from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater
from telegram.utils.helpers import escape_markdown from telegram.utils.helpers import escape_markdown
@@ -35,6 +35,8 @@ logger = logging.getLogger(__name__)
logger.debug('Included module rpc.telegram ...') logger.debug('Included module rpc.telegram ...')
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
@dataclass @dataclass
class TimeunitMappings: class TimeunitMappings:
@@ -70,7 +72,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
) )
return wrapper return wrapper
# Rollback session to avoid getting data stored in a transaction. # Rollback session to avoid getting data stored in a transaction.
Trade.rollback() Trade.query.session.rollback()
logger.debug( logger.debug(
'Executing handler: %s for chat_id: %s', 'Executing handler: %s for chat_id: %s',
command_handler.__name__, command_handler.__name__,
@@ -120,8 +122,7 @@ class Telegram(RPCHandler):
r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+', r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+',
r'/stats$', r'/count$', r'/locks$', r'/balance$', r'/stats$', r'/count$', r'/locks$', r'/balance$',
r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/stopbuy$', r'/reload_config$', r'/show_config$',
r'/logs$', r'/whitelist$', r'/whitelist(\ssorted|\sbaseonly)+$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
r'/blacklist$', r'/bl_delete$',
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
r'/forcebuy$', r'/forcelong$', r'/forceshort$', r'/forcebuy$', r'/forcelong$', r'/forceshort$',
r'/forcesell$', r'/forceexit$', r'/forcesell$', r'/forceexit$',
@@ -314,36 +315,20 @@ class Telegram(RPCHandler):
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
msg['profit_extra'] = ( msg['profit_extra'] = (
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}") f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})")
else: else:
msg['profit_extra'] = '' msg['profit_extra'] = ''
msg['profit_extra'] = (
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
f"{msg['profit_extra']})")
is_fill = msg['type'] == RPCMessageType.EXIT_FILL is_fill = msg['type'] == RPCMessageType.EXIT_FILL
is_sub_trade = msg.get('sub_trade')
is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit')
profit_prefix = ('Sub ' if is_sub_profit
else 'Cumulative ') if is_sub_trade else ''
cp_extra = ''
if is_sub_profit and is_sub_trade:
if self._rpc._fiat_converter:
cp_fiat = self._rpc._fiat_converter.convert_amount(
msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency'])
cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}"
else:
cp_extra = ''
cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \
f"{msg['stake_currency']}{cp_extra}`)\n"
message = ( message = (
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* " f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
f"{self._add_analyzed_candle(msg['pair'])}" f"{self._add_analyzed_candle(msg['pair'])}"
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* " f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
f"{cp_extra}"
f"*Enter Tag:* `{msg['enter_tag']}`\n" f"*Enter Tag:* `{msg['enter_tag']}`\n"
f"*Exit Reason:* `{msg['exit_reason']}`\n" f"*Exit Reason:* `{msg['exit_reason']}`\n"
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
f"*Direction:* `{msg['direction']}`\n" f"*Direction:* `{msg['direction']}`\n"
f"{msg['leverage_text']}" f"{msg['leverage_text']}"
f"*Amount:* `{msg['amount']:.8f}`\n" f"*Amount:* `{msg['amount']:.8f}`\n"
@@ -351,25 +336,11 @@ class Telegram(RPCHandler):
) )
if msg['type'] == RPCMessageType.EXIT: if msg['type'] == RPCMessageType.EXIT:
message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n" message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
f"*Exit Rate:* `{msg['limit']:.8f}`") f"*Close Rate:* `{msg['limit']:.8f}`")
elif msg['type'] == RPCMessageType.EXIT_FILL: elif msg['type'] == RPCMessageType.EXIT_FILL:
message += f"*Exit Rate:* `{msg['close_rate']:.8f}`" message += f"*Close Rate:* `{msg['close_rate']:.8f}`"
if msg.get('sub_trade'):
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
else:
msg['stake_amount_fiat'] = 0
rem = round_coin_value(msg['stake_amount'], msg['stake_currency'])
message += f"\n*Remaining:* `({rem}"
if msg.get('fiat_currency', None):
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
message += ")`"
else:
message += f"\n*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`"
return message return message
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
@@ -382,8 +353,7 @@ class Telegram(RPCHandler):
elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL): elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit' msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* " message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* "
f"Cancelling {'partial ' if msg.get('sub_trade') else ''}" f"Cancelling {msg['message_side']} Order for {msg['pair']} "
f"{msg['message_side']} Order for {msg['pair']} "
f"(#{msg['trade_id']}). Reason: {msg['reason']}.") f"(#{msg['trade_id']}). Reason: {msg['reason']}.")
elif msg_type == RPCMessageType.PROTECTION_TRIGGER: elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
@@ -406,8 +376,7 @@ class Telegram(RPCHandler):
elif msg_type == RPCMessageType.STARTUP: elif msg_type == RPCMessageType.STARTUP:
message = f"{msg['status']}" message = f"{msg['status']}"
elif msg_type == RPCMessageType.STRATEGY_MSG:
message = f"{msg['msg']}"
else: else:
raise NotImplementedError(f"Unknown message type: {msg_type}") raise NotImplementedError(f"Unknown message type: {msg_type}")
return message return message
@@ -454,63 +423,54 @@ class Telegram(RPCHandler):
else: else:
return "\N{CROSS MARK}" return "\N{CROSS MARK}"
def _prepare_order_details(self, filled_orders: List, quote_currency: str, is_open: bool): def _prepare_entry_details(self, filled_orders: List, quote_currency: str, is_open: bool):
""" """
Prepare details of trade with entry adjustment enabled Prepare details of trade with entry adjustment enabled
""" """
lines_detail: List[str] = [] lines: List[str] = []
if len(filled_orders) > 0: if len(filled_orders) > 0:
first_avg = filled_orders[0]["safe_price"] first_avg = filled_orders[0]["safe_price"]
for x, order in enumerate(filled_orders): for x, order in enumerate(filled_orders):
lines: List[str] = [] if not order['ft_is_entry'] or order['is_open'] is True:
if order['is_open'] is True:
continue continue
wording = 'Entry' if order['ft_is_entry'] else 'Exit'
cur_entry_datetime = arrow.get(order["order_filled_date"]) cur_entry_datetime = arrow.get(order["order_filled_date"])
cur_entry_amount = order["filled"] or order["amount"] cur_entry_amount = order["amount"]
cur_entry_average = order["safe_price"] cur_entry_average = order["safe_price"]
lines.append(" ") lines.append(" ")
if x == 0: if x == 0:
lines.append(f"*{wording} #{x+1}:*") lines.append(f"*Entry #{x+1}:*")
lines.append( lines.append(
f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
lines.append(f"*Average Price:* {cur_entry_average}") lines.append(f"*Average Entry Price:* {cur_entry_average}")
else: else:
sumA = 0 sumA = 0
sumB = 0 sumB = 0
for y in range(x): for y in range(x):
amount = filled_orders[y]["filled"] or filled_orders[y]["amount"] sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"])
sumA += amount * filled_orders[y]["safe_price"] sumB += filled_orders[y]["amount"]
sumB += amount
prev_avg_price = sumA / sumB prev_avg_price = sumA / sumB
# TODO: This calculation ignores fees.
price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg) price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
minus_on_entry = 0 minus_on_entry = 0
if prev_avg_price: if prev_avg_price:
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
lines.append(f"*{wording} #{x+1}:* at {minus_on_entry:.2%} avg profit") dur_entry = cur_entry_datetime - arrow.get(
filled_orders[x - 1]["order_filled_date"])
days = dur_entry.days
hours, remainder = divmod(dur_entry.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
lines.append(f"*Entry #{x+1}:* at {minus_on_entry:.2%} avg profit")
if is_open: if is_open:
lines.append("({})".format(cur_entry_datetime lines.append("({})".format(cur_entry_datetime
.humanize(granularity=["day", "hour", "minute"]))) .humanize(granularity=["day", "hour", "minute"])))
lines.append( lines.append(
f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
lines.append(f"*Average {wording} Price:* {cur_entry_average} " lines.append(f"*Average Entry Price:* {cur_entry_average} "
f"({price_to_1st_entry:.2%} from 1st entry rate)") f"({price_to_1st_entry:.2%} from 1st entry rate)")
lines.append(f"*Order filled:* {order['order_filled_date']}") lines.append(f"*Order filled at:* {order['order_filled_date']}")
lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)")
# TODO: is this really useful? return lines
# dur_entry = cur_entry_datetime - arrow.get(
# filled_orders[x - 1]["order_filled_date"])
# days = dur_entry.days
# hours, remainder = divmod(dur_entry.seconds, 3600)
# minutes, seconds = divmod(remainder, 60)
# lines.append(
# f"({days}d {hours}h {minutes}m {seconds}s from previous {wording.lower()})")
lines_detail.append("\n".join(lines))
return lines_detail
@authorized_only @authorized_only
def _status(self, update: Update, context: CallbackContext) -> None: def _status(self, update: Update, context: CallbackContext) -> None:
@@ -525,14 +485,7 @@ class Telegram(RPCHandler):
if context.args and 'table' in context.args: if context.args and 'table' in context.args:
self._status_table(update, context) self._status_table(update, context)
return return
else:
self._status_msg(update, context)
def _status_msg(self, update: Update, context: CallbackContext) -> None:
"""
handler for `/status` and `/status <id>`.
"""
try: try:
# Check if there's at least one numerical ID provided. # Check if there's at least one numerical ID provided.
@@ -544,13 +497,14 @@ class Telegram(RPCHandler):
results = self._rpc._rpc_trade_status(trade_ids=trade_ids) results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
position_adjust = self._config.get('position_adjustment_enable', False) position_adjust = self._config.get('position_adjustment_enable', False)
max_entries = self._config.get('max_entry_position_adjustment', -1) max_entries = self._config.get('max_entry_position_adjustment', -1)
messages = []
for r in results: for r in results:
r['open_date_hum'] = arrow.get(r['open_date']).humanize() r['open_date_hum'] = arrow.get(r['open_date']).humanize()
r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']]) r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']])
r['exit_reason'] = r.get('exit_reason', "") r['exit_reason'] = r.get('exit_reason', "")
lines = [ lines = [
"*Trade ID:* `{trade_id}`" + "*Trade ID:* `{trade_id}`" +
(" `(since {open_date_hum})`" if r['is_open'] else ""), ("` (since {open_date_hum})`" if r['is_open'] else ""),
"*Current Pair:* {pair}", "*Current Pair:* {pair}",
"*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"), "*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
"*Leverage:* `{leverage}`" if r.get('leverage') else "", "*Leverage:* `{leverage}`" if r.get('leverage') else "",
@@ -574,8 +528,6 @@ class Telegram(RPCHandler):
]) ])
if r['is_open']: if r['is_open']:
if r.get('realized_profit'):
lines.append("*Realized Profit:* `{realized_profit:.8f}`")
if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
and r['initial_stop_loss_ratio'] is not None): and r['initial_stop_loss_ratio'] is not None):
# Adding initial stoploss only if it is different from stoploss # Adding initial stoploss only if it is different from stoploss
@@ -588,34 +540,24 @@ class Telegram(RPCHandler):
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
"`({stoploss_current_dist_ratio:.2%})`") "`({stoploss_current_dist_ratio:.2%})`")
if r['open_order']: if r['open_order']:
lines.append( if r['exit_order_status']:
"*Open Order:* `{open_order}`" lines.append("*Open Order:* `{open_order}` - `{exit_order_status}`")
+ "- `{exit_order_status}`" if r['exit_order_status'] else "") else:
lines.append("*Open Order:* `{open_order}`")
lines_detail = self._prepare_order_details( lines_detail = self._prepare_entry_details(
r['orders'], r['quote_currency'], r['is_open']) r['orders'], r['quote_currency'], r['is_open'])
lines.extend(lines_detail if lines_detail else "") lines.extend(lines_detail if lines_detail else "")
self.__send_status_msg(lines, r)
# Filter empty lines using list-comprehension
messages.append("\n".join([line for line in lines if line]).format(**r))
for msg in messages:
self._send_msg(msg)
except RPCException as e: except RPCException as e:
self._send_msg(str(e)) self._send_msg(str(e))
def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
"""
Send status message.
"""
msg = ''
for line in lines:
if line:
if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
msg += line + '\n'
else:
self._send_msg(msg.format(**r))
msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n'
self._send_msg(msg.format(**r))
@authorized_only @authorized_only
def _status_table(self, update: Update, context: CallbackContext) -> None: def _status_table(self, update: Update, context: CallbackContext) -> None:
""" """
@@ -918,7 +860,7 @@ class Telegram(RPCHandler):
total_dust_currencies += 1 total_dust_currencies += 1
# Handle overflowing message length # Handle overflowing message length
if len(output + curr_output) >= MAX_MESSAGE_LENGTH: if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output) self._send_msg(output)
output = curr_output output = curr_output
else: else:
@@ -1181,7 +1123,7 @@ class Telegram(RPCHandler):
f"({trade['profit_ratio']:.2%}) " f"({trade['profit_ratio']:.2%}) "
f"({trade['count']})</code>\n") f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_MESSAGE_LENGTH: if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML) self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line output = stat_line
else: else:
@@ -1216,7 +1158,7 @@ class Telegram(RPCHandler):
f"({trade['profit_ratio']:.2%}) " f"({trade['profit_ratio']:.2%}) "
f"({trade['count']})</code>\n") f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_MESSAGE_LENGTH: if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML) self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line output = stat_line
else: else:
@@ -1251,7 +1193,7 @@ class Telegram(RPCHandler):
f"({trade['profit_ratio']:.2%}) " f"({trade['profit_ratio']:.2%}) "
f"({trade['count']})</code>\n") f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_MESSAGE_LENGTH: if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML) self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line output = stat_line
else: else:
@@ -1286,7 +1228,7 @@ class Telegram(RPCHandler):
f"({trade['profit']:.2%}) " f"({trade['profit']:.2%}) "
f"({trade['count']})</code>\n") f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_MESSAGE_LENGTH: if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML) self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line output = stat_line
else: else:
@@ -1369,12 +1311,6 @@ class Telegram(RPCHandler):
try: try:
whitelist = self._rpc._rpc_whitelist() whitelist = self._rpc._rpc_whitelist()
if context.args:
if "sorted" in context.args:
whitelist['whitelist'] = sorted(whitelist['whitelist'])
if "baseonly" in context.args:
whitelist['whitelist'] = [pair.split("/")[0] for pair in whitelist['whitelist']]
message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n" message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n"
message += f"`{', '.join(whitelist['whitelist'])}`" message += f"`{', '.join(whitelist['whitelist'])}`"
@@ -1431,7 +1367,7 @@ class Telegram(RPCHandler):
escape_markdown(logrec[2], version=2), escape_markdown(logrec[2], version=2),
escape_markdown(logrec[3], version=2), escape_markdown(logrec[3], version=2),
escape_markdown(logrec[4], version=2)) escape_markdown(logrec[4], version=2))
if len(msgs + msg) + 10 >= MAX_MESSAGE_LENGTH: if len(msgs + msg) + 10 >= MAX_TELEGRAM_MESSAGE_LENGTH:
# Send message immediately if it would become too long # Send message immediately if it would become too long
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
msgs = msg + '\n' msgs = msg + '\n'
@@ -1494,8 +1430,7 @@ 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"
"*/whitelist [sorted] [baseonly]:* `Show current whitelist. Optionally in " "*/whitelist:* `Show current whitelist` \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 "
"to the blacklist.` \n" "to the blacklist.` \n"
"*/blacklist_delete [pairs]| /bl_delete [pairs]:* " "*/blacklist_delete [pairs]| /bl_delete [pairs]:* "
@@ -1532,7 +1467,7 @@ class Telegram(RPCHandler):
"*/weekly <n>:* `Shows statistics per week, over the last n weeks`\n" "*/weekly <n>:* `Shows statistics per week, over the last n weeks`\n"
"*/monthly <n>:* `Shows statistics per month, over the last n months`\n" "*/monthly <n>:* `Shows statistics per month, over the last n months`\n"
"*/stats:* `Shows Wins / losses by Sell reason as well as " "*/stats:* `Shows Wins / losses by Sell reason as well as "
"Avg. holding durations for buys and sells.`\n" "Avg. holding durationsfor buys and sells.`\n"
"*/help:* `This help message`\n" "*/help:* `This help message`\n"
"*/version:* `Show version`" "*/version:* `Show version`"
) )

View File

@@ -145,29 +145,11 @@ class IStrategy(ABC, HyperStrategyMixin):
informative_data.candle_type = config['candle_type_def'] informative_data.candle_type = config['candle_type_def']
self._ft_informative.append((informative_data, cls_method)) self._ft_informative.append((informative_data, cls_method))
def load_freqAI_model(self) -> None:
if self.config.get('freqai', {}).get('enabled', False):
# Import here to avoid importing this if freqAI is disabled
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
self.freqai = FreqaiModelResolver.load_freqaimodel(self.config)
self.freqai_info = self.config["freqai"]
else:
# Gracious failures if freqAI is disabled but "start" is called.
class DummyClass():
def start(self, *args, **kwargs):
raise OperationalException(
'freqAI is not enabled. '
'Please enable it in your config to use this strategy.')
self.freqai = DummyClass() # type: ignore
def ft_bot_start(self, **kwargs) -> None: def ft_bot_start(self, **kwargs) -> None:
""" """
Strategy init - runs after dataprovider has been added. Strategy init - runs after dataprovider has been added.
Must call bot_start() Must call bot_start()
""" """
self.load_freqAI_model()
strategy_safe_wrapper(self.bot_start)() strategy_safe_wrapper(self.bot_start)()
self.ft_load_hyper_params(self.config.get('runmode') == RunMode.HYPEROPT) self.ft_load_hyper_params(self.config.get('runmode') == RunMode.HYPEROPT)
@@ -481,13 +463,10 @@ class IStrategy(ABC, HyperStrategyMixin):
def adjust_trade_position(self, trade: Trade, current_time: datetime, def adjust_trade_position(self, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, current_rate: float, current_profit: float,
min_stake: Optional[float], max_stake: float, min_stake: Optional[float], max_stake: float,
current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float,
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
""" """
Custom trade adjustment logic, returning the stake amount that a trade should be Custom trade adjustment logic, returning the stake amount that a trade should be increased.
increased or decreased. This means extra buy orders with additional fees.
This means extra buy or sell orders with additional fees.
Only called when `position_adjustment_enable` is set to True. Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
@@ -498,16 +477,10 @@ class IStrategy(ABC, HyperStrategyMixin):
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param current_rate: Current buy rate. :param current_rate: Current buy rate.
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate.
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits) :param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Maximum stake allowed (either through balance, or by exchange limits). :param max_stake: Balance available for trading.
:param current_entry_rate: Current rate using entry pricing.
:param current_exit_rate: Current rate using exit pricing.
:param current_entry_profit: Current profit using entry pricing.
:param current_exit_profit: Current profit using exit pricing.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: Stake amount to adjust your trade, :return float: Stake amount to adjust your trade
Positive values to increase position, Negative values to decrease position.
Return None for no action.
""" """
return None return None
@@ -575,22 +548,6 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return None return None
def populate_any_indicators(self, pair: str, df: DataFrame, tf: str,
informative: DataFrame = None,
set_generalized_indicators: bool = False) -> DataFrame:
"""
Function designed to automatically generate, name and merge features
from user indicated timeframes in the configuration file. User can add
additional features here, but must follow the naming convention.
This method is *only* used in FreqaiDataKitchen class and therefore
it is only called if FreqAI is active.
:param pair: pair to be used as informative
:param df: strategy dataframe which will receive merges from informatives
:param tf: timeframe of the dataframe which will modify the feature names
:param informative: the dataframe associated with the informative pair
"""
return df
### ###
# END - Intended to be overridden by strategy # END - Intended to be overridden by strategy
### ###
@@ -617,6 +574,9 @@ class IStrategy(ABC, HyperStrategyMixin):
) )
informative_pairs.append(pair_tf) informative_pairs.append(pair_tf)
else: else:
if not self.dp:
raise OperationalException('@informative decorator with unspecified asset '
'requires DataProvider instance.')
for pair in self.dp.current_whitelist(): for pair in self.dp.current_whitelist():
informative_pairs.append((pair, inf_data.timeframe, candle_type)) informative_pairs.append((pair, inf_data.timeframe, candle_type))
return list(set(informative_pairs)) return list(set(informative_pairs))
@@ -710,6 +670,7 @@ class IStrategy(ABC, HyperStrategyMixin):
# Defs that only make change on new candle data. # Defs that only make change on new candle data.
dataframe = self.analyze_ticker(dataframe, metadata) dataframe = self.analyze_ticker(dataframe, metadata)
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
if self.dp:
self.dp._set_cached_df( self.dp._set_cached_df(
pair, self.timeframe, dataframe, pair, self.timeframe, dataframe,
candle_type=self.config.get('candle_type_def', CandleType.SPOT)) candle_type=self.config.get('candle_type_def', CandleType.SPOT))
@@ -733,6 +694,8 @@ class IStrategy(ABC, HyperStrategyMixin):
The analyzed dataframe is then accessible via `dp.get_analyzed_dataframe()`. The analyzed dataframe is then accessible via `dp.get_analyzed_dataframe()`.
:param pair: Pair to analyze. :param pair: Pair to analyze.
""" """
if not self.dp:
raise OperationalException("DataProvider not found.")
dataframe = self.dp.ohlcv( dataframe = self.dp.ohlcv(
pair, self.timeframe, candle_type=self.config.get('candle_type_def', CandleType.SPOT) pair, self.timeframe, candle_type=self.config.get('candle_type_def', CandleType.SPOT)
) )
@@ -1000,7 +963,7 @@ class IStrategy(ABC, HyperStrategyMixin):
# ROI # ROI
# Trailing stoploss # Trailing stoploss
if stoplossflag.exit_type in (ExitType.STOP_LOSS, ExitType.LIQUIDATION): if stoplossflag.exit_type == ExitType.STOP_LOSS:
logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}") logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}")
exits.append(stoplossflag) exits.append(stoplossflag)
@@ -1072,17 +1035,6 @@ class IStrategy(ABC, HyperStrategyMixin):
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
and trade.liquidation_price >= (low or current_rate)
and not trade.is_short)
liq_lower_short = (trade.liquidation_price
and trade.liquidation_price <= (high or current_rate)
and trade.is_short)
if (liq_higher_long or liq_lower_short):
logger.debug(f"{trade.pair} - Liquidation price hit. exit_type=ExitType.LIQUIDATION")
return ExitCheckTuple(exit_type=ExitType.LIQUIDATION)
# evaluate if the stoploss was hit if stoploss is not on exchange # evaluate if the stoploss was hit if stoploss is not on exchange
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
# regular stoploss handling. # regular stoploss handling.
@@ -1100,6 +1052,13 @@ class IStrategy(ABC, HyperStrategyMixin):
f"stoploss is {trade.stop_loss:.6f}, " f"stoploss is {trade.stop_loss:.6f}, "
f"initial stoploss was at {trade.initial_stop_loss:.6f}, " f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
f"trade opened at {trade.open_rate:.6f}") f"trade opened at {trade.open_rate:.6f}")
new_stoploss = (
trade.stop_loss + trade.initial_stop_loss
if trade.is_short else
trade.stop_loss - trade.initial_stop_loss
)
logger.debug(f"{trade.pair} - Trailing stop saved "
f"{new_stoploss:.6f}")
return ExitCheckTuple(exit_type=exit_type) return ExitCheckTuple(exit_type=exit_type)

View File

@@ -1,328 +0,0 @@
import logging
from functools import reduce
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from technical import qtpylib
from freqtrade.exchange import timeframe_to_prev_date
from freqtrade.persistence import Trade
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair
logger = logging.getLogger(__name__)
class FreqaiExampleStrategy(IStrategy):
"""
Example strategy showing how the user connects their own
IFreqaiModel to the strategy. Namely, the user uses:
self.freqai.start(dataframe, metadata)
to make predictions on their data. populate_any_indicators() automatically
generates the variety of features indicated by the user in the
canonical freqtrade configuration file under config['freqai'].
"""
minimal_roi = {"0": 0.1, "240": -1}
plot_config = {
"main_plot": {},
"subplots": {
"prediction": {"prediction": {"color": "blue"}},
"target_roi": {
"target_roi": {"color": "brown"},
},
"do_predict": {
"do_predict": {"color": "brown"},
},
},
}
process_only_new_candles = True
stoploss = -0.05
use_exit_signal = True
startup_candle_count: int = 300
can_short = False
linear_roi_offset = DecimalParameter(
0.00, 0.02, default=0.005, space="sell", optimize=False, load=True
)
max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True)
def informative_pairs(self):
whitelist_pairs = self.dp.current_whitelist()
corr_pairs = self.config["freqai"]["feature_parameters"]["include_corr_pairlist"]
informative_pairs = []
for tf in self.config["freqai"]["feature_parameters"]["include_timeframes"]:
for pair in whitelist_pairs:
informative_pairs.append((pair, tf))
for pair in corr_pairs:
if pair in whitelist_pairs:
continue # avoid duplication
informative_pairs.append((pair, tf))
return informative_pairs
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
"""
Function designed to automatically generate, name and merge features
from user indicated timeframes in the configuration file. User controls the indicators
passed to the training/prediction by prepending indicators with `'%-' + coin `
(see convention below). I.e. user should not prepend any supporting metrics
(e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the
model.
:param pair: pair to be used as informative
:param df: strategy dataframe which will receive merges from informatives
:param tf: timeframe of the dataframe which will modify the feature names
:param informative: the dataframe associated with the informative pair
"""
coin = pair.split('/')[0]
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
t = int(t)
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
informative[f"%-{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
informative[f"%-{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t)
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(informative), window=t, stds=2.2
)
informative[f"{coin}bb_lowerband-period_{t}"] = bollinger["lower"]
informative[f"{coin}bb_middleband-period_{t}"] = bollinger["mid"]
informative[f"{coin}bb_upperband-period_{t}"] = bollinger["upper"]
informative[f"%-{coin}bb_width-period_{t}"] = (
informative[f"{coin}bb_upperband-period_{t}"]
- informative[f"{coin}bb_lowerband-period_{t}"]
) / informative[f"{coin}bb_middleband-period_{t}"]
informative[f"%-{coin}close-bb_lower-period_{t}"] = (
informative["close"] / informative[f"{coin}bb_lowerband-period_{t}"]
)
informative[f"%-{coin}roc-period_{t}"] = ta.ROC(informative, timeperiod=t)
informative[f"%-{coin}relative_volume-period_{t}"] = (
informative["volume"] / informative["volume"].rolling(t).mean()
)
informative[f"%-{coin}pct-change"] = informative["close"].pct_change()
informative[f"%-{coin}raw_volume"] = informative["volume"]
informative[f"%-{coin}raw_price"] = informative["close"]
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
# user adds targets here by prepending them with &- (see convention below)
df["&-s_close"] = (
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ df["close"]
- 1
)
# Classifiers are typically set up with strings as targets:
# df['&s-up_or_down'] = np.where( df["close"].shift(-100) >
# df["close"], 'up', 'down')
# If user wishes to use multiple targets, they can add more by
# appending more columns with '&'. User should keep in mind that multi targets
# requires a multioutput prediction model such as
# templates/CatboostPredictionMultiModel.py,
# df["&-s_range"] = (
# df["close"]
# .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
# .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
# .max()
# -
# df["close"]
# .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
# .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
# .min()
# )
return df
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# All indicators must be populated by populate_any_indicators() for live functionality
# to work correctly.
# the model will return all labels created by user in `populate_any_indicators`
# (& appended targets), an indication of whether or not the prediction should be accepted,
# the target mean/std values for each of the labels created by user in
# `populate_any_indicators()` for each training period.
dataframe = self.freqai.start(dataframe, metadata, self)
dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25
dataframe["sell_roi"] = dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * 1.25
return dataframe
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
enter_long_conditions = [df["do_predict"] == 1, df["&-s_close"] > df["target_roi"]]
if enter_long_conditions:
df.loc[
reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"]
] = (1, "long")
enter_short_conditions = [df["do_predict"] == 1, df["&-s_close"] < df["sell_roi"]]
if enter_short_conditions:
df.loc[
reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"]
] = (1, "short")
return df
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
exit_long_conditions = [df["do_predict"] == 1, df["&-s_close"] < df["sell_roi"] * 0.25]
if exit_long_conditions:
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1
exit_short_conditions = [df["do_predict"] == 1, df["&-s_close"] > df["target_roi"] * 0.25]
if exit_short_conditions:
df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1
return df
def get_ticker_indicator(self):
return int(self.config["timeframe"][:-1])
def custom_exit(
self, pair: str, trade: Trade, current_time, current_rate, current_profit, **kwargs
):
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
trade_date = timeframe_to_prev_date(self.config["timeframe"], trade.open_date_utc)
trade_candle = dataframe.loc[(dataframe["date"] == trade_date)]
if trade_candle.empty:
return None
trade_candle = trade_candle.squeeze()
follow_mode = self.config.get("freqai", {}).get("follow_mode", False)
if not follow_mode:
pair_dict = self.freqai.dd.pair_dict
else:
pair_dict = self.freqai.dd.follower_dict
entry_tag = trade.enter_tag
if (
"prediction" + entry_tag not in pair_dict[pair]
or pair_dict[pair]['extras']["prediction" + entry_tag] == 0
):
pair_dict[pair]['extras']["prediction" + entry_tag] = abs(trade_candle["&-s_close"])
if not follow_mode:
self.freqai.dd.save_drawer_to_disk()
else:
self.freqai.dd.save_follower_dict_to_disk()
roi_price = pair_dict[pair]['extras']["prediction" + entry_tag]
roi_time = self.max_roi_time_long.value
roi_decay = roi_price * (
1 - ((current_time - trade.open_date_utc).seconds) / (roi_time * 60)
)
if roi_decay < 0:
roi_decay = self.linear_roi_offset.value
else:
roi_decay += self.linear_roi_offset.value
if current_profit > roi_decay:
return "roi_custom_win"
if current_profit < -roi_decay:
return "roi_custom_loss"
def confirm_trade_exit(
self,
pair: str,
trade: Trade,
order_type: str,
amount: float,
rate: float,
time_in_force: str,
exit_reason: str,
current_time,
**kwargs,
) -> bool:
entry_tag = trade.enter_tag
follow_mode = self.config.get("freqai", {}).get("follow_mode", False)
if not follow_mode:
pair_dict = self.freqai.dd.pair_dict
else:
pair_dict = self.freqai.dd.follower_dict
pair_dict[pair]['extras']["prediction" + entry_tag] = 0
if not follow_mode:
self.freqai.dd.save_drawer_to_disk()
else:
self.freqai.dd.save_follower_dict_to_disk()
return True
def confirm_trade_entry(
self,
pair: str,
order_type: str,
amount: float,
rate: float,
time_in_force: str,
current_time,
entry_tag,
side: str,
**kwargs,
) -> bool:
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = df.iloc[-1].squeeze()
if side == "long":
if rate > (last_candle["close"] * (1 + 0.0025)):
return False
else:
if rate < (last_candle["close"] * (1 - 0.0025)):
return False
return True

View File

@@ -12,7 +12,6 @@
"tradable_balance_ratio": 0.99, "tradable_balance_ratio": 0.99,
"fiat_display_currency": "{{ fiat_display_currency }}",{{ ('\n "timeframe": "' + timeframe + '",') if timeframe else '' }} "fiat_display_currency": "{{ fiat_display_currency }}",{{ ('\n "timeframe": "' + timeframe + '",') if timeframe else '' }}
"dry_run": {{ dry_run | lower }}, "dry_run": {{ dry_run | lower }},
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": false, "cancel_open_orders_on_exit": false,
"trading_mode": "{{ trading_mode }}", "trading_mode": "{{ trading_mode }}",
"margin_mode": "{{ margin_mode }}", "margin_mode": "{{ margin_mode }}",

View File

@@ -30,7 +30,7 @@
"\n", "\n",
"# Initialize empty configuration object\n", "# Initialize empty configuration object\n",
"config = Configuration.from_files([])\n", "config = Configuration.from_files([])\n",
"# Optionally (recommended), use existing configuration file\n", "# Optionally, use existing configuration file\n",
"# config = Configuration.from_files([\"config.json\"])\n", "# config = Configuration.from_files([\"config.json\"])\n",
"\n", "\n",
"# Define some constants\n", "# Define some constants\n",
@@ -38,7 +38,7 @@
"# Name of the strategy class\n", "# Name of the strategy class\n",
"config[\"strategy\"] = \"SampleStrategy\"\n", "config[\"strategy\"] = \"SampleStrategy\"\n",
"# Location of the data\n", "# Location of the data\n",
"data_location = config['datadir']\n", "data_location = Path(config['user_data_dir'], 'data', 'binance')\n",
"# Pair to analyze - Only use one pair here\n", "# Pair to analyze - Only use one pair here\n",
"pair = \"BTC/USDT\"" "pair = \"BTC/USDT\""
] ]
@@ -365,7 +365,7 @@
"metadata": { "metadata": {
"file_extension": ".py", "file_extension": ".py",
"kernelspec": { "kernelspec": {
"display_name": "Python 3.9.7 64-bit ('trade_397')", "display_name": "Python 3",
"language": "python", "language": "python",
"name": "python3" "name": "python3"
}, },
@@ -379,7 +379,7 @@
"name": "python", "name": "python",
"nbconvert_exporter": "python", "nbconvert_exporter": "python",
"pygments_lexer": "ipython3", "pygments_lexer": "ipython3",
"version": "3.9.7" "version": "3.8.5"
}, },
"mimetype": "text/x-python", "mimetype": "text/x-python",
"name": "python", "name": "python",
@@ -427,12 +427,7 @@
], ],
"window_display": false "window_display": false
}, },
"version": 3, "version": 3
"vscode": {
"interpreter": {
"hash": "675f32a300d6d26767470181ad0b11dd4676bcce7ed1dd2ffe2fbc370c95fc7c"
}
}
}, },
"nbformat": 4, "nbformat": 4,
"nbformat_minor": 4 "nbformat_minor": 4

View File

@@ -1,12 +0,0 @@
"exchange": {
"name": "{{ exchange_name | lower }}",
"key": "{{ exchange_key }}",
"secret": "{{ exchange_secret }}",
"unknown_fee_rate": 1,
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
],
"pair_blacklist": [
]
}

View File

@@ -5,7 +5,9 @@
"ccxt_config": {}, "ccxt_config": {},
"ccxt_async_config": {}, "ccxt_async_config": {},
"pair_whitelist": [ "pair_whitelist": [
], ],
"pair_blacklist": [ "pair_blacklist": [
] ]
} }

View File

@@ -247,16 +247,12 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
""" """
return False return False
def adjust_trade_position(self, trade: 'Trade', current_time: datetime, def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
current_rate: float, current_profit: float, current_rate: float, current_profit: float, min_stake: Optional[float],
min_stake: Optional[float], max_stake: float, max_stake: float, **kwargs) -> 'Optional[float]':
current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float,
**kwargs) -> Optional[float]:
""" """
Custom trade adjustment logic, returning the stake amount that a trade should be Custom trade adjustment logic, returning the stake amount that a trade should be increased.
increased or decreased. This means extra buy orders with additional fees.
This means extra buy or sell orders with additional fees.
Only called when `position_adjustment_enable` is set to True. Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
@@ -267,16 +263,10 @@ def adjust_trade_position(self, trade: 'Trade', current_time: datetime,
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param current_rate: Current buy rate. :param current_rate: Current buy rate.
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate.
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits) :param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Maximum stake allowed (either through balance, or by exchange limits). :param max_stake: Balance available for trading.
:param current_entry_rate: Current rate using entry pricing.
:param current_exit_rate: Current rate using exit pricing.
:param current_entry_profit: Current profit using entry pricing.
:param current_exit_profit: Current profit using exit pricing.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: Stake amount to adjust your trade, :return float: Stake amount to adjust your trade
Positive values to increase position, Negative values to decrease position.
Return None for no action.
""" """
return None return None

View File

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

View File

@@ -1,12 +0,0 @@
"""
Slim wrapper around ccxt's Precise (string math)
To have imports from freqtrade - and support float initializers
"""
from ccxt import Precise
class FtPrecise(Precise):
def __init__(self, number, decimals=None):
if not isinstance(number, str):
number = str(number)
super().__init__(number, decimals)

View File

@@ -148,7 +148,7 @@ class Wallets:
# Position is not open ... # Position is not open ...
continue continue
size = self._exchange._contracts_to_amount(symbol, position['contracts']) size = self._exchange._contracts_to_amount(symbol, position['contracts'])
collateral = position['collateral'] or 0.0 collateral = position['collateral']
leverage = position['leverage'] leverage = position['leverage']
self._positions[symbol] = PositionWallet( self._positions[symbol] = PositionWallet(
symbol, position=size, symbol, position=size,

View File

@@ -1,7 +1,6 @@
site_name: Freqtrade site_name: Freqtrade
site_url: https://www.freqtrade.io/ site_url: https://www.freqtrade.io/
repo_url: https://github.com/freqtrade/freqtrade repo_url: https://github.com/freqtrade/freqtrade
edit_uri: edit/develop/docs/
use_directory_urls: True use_directory_urls: True
nav: nav:
- Home: index.md - Home: index.md
@@ -33,10 +32,9 @@ nav:
- Backtest analysis: advanced-backtesting.md - Backtest analysis: advanced-backtesting.md
- Advanced Topics: - Advanced Topics:
- Advanced Post-installation Tasks: advanced-setup.md - Advanced Post-installation Tasks: advanced-setup.md
- Edge Positioning: edge.md
- Advanced Strategy: strategy-advanced.md - Advanced Strategy: strategy-advanced.md
- Advanced Hyperopt: advanced-hyperopt.md - Advanced Hyperopt: advanced-hyperopt.md
- FreqAI: freqai.md
- Edge Positioning: edge.md
- Sandbox Testing: sandbox-testing.md - Sandbox Testing: sandbox-testing.md
- FAQ: faq.md - FAQ: faq.md
- SQL Cheat-sheet: sql_cheatsheet.md - SQL Cheat-sheet: sql_cheatsheet.md

View File

@@ -2,11 +2,10 @@
-r requirements.txt -r requirements.txt
-r requirements-plot.txt -r requirements-plot.txt
-r requirements-hyperopt.txt -r requirements-hyperopt.txt
-r requirements-freqai.txt
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
coveralls==3.3.1 coveralls==3.3.1
flake8==5.0.4 flake8==4.0.1
flake8-tidy-imports==4.8.0 flake8-tidy-imports==4.8.0
mypy==0.971 mypy==0.971
pre-commit==2.20.0 pre-commit==2.20.0
@@ -20,11 +19,11 @@ isort==5.10.1
time-machine==2.7.1 time-machine==2.7.1
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==6.5.3 nbconvert==6.5.0
# mypy types # mypy types
types-cachetools==5.2.1 types-cachetools==5.2.1
types-filelock==3.2.7 types-filelock==3.2.7
types-requests==2.28.8 types-requests==2.28.3
types-tabulate==0.8.11 types-tabulate==0.8.11
types-python-dateutil==2.8.19 types-python-dateutil==2.8.19

View File

@@ -1,8 +0,0 @@
# Include all requirements to run the bot.
-r requirements.txt
# Required for freqai
scikit-learn==1.1.2
joblib==1.1.0
catboost==1.0.6; platform_machine != 'aarch64'
lightgbm==3.3.2

View File

@@ -2,8 +2,8 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # Required for hyperopt
scipy==1.9.0 scipy==1.8.1
scikit-learn==1.1.2 scikit-learn==1.1.1
scikit-optimize==0.9.0 scikit-optimize==0.9.0
filelock==3.8.0 filelock==3.7.1
progressbar2==4.0.0 progressbar2==4.0.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.10.0 plotly==5.9.0

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