Merge pull request #10599 from freqtrade/new_release

New release 2024.8
This commit is contained in:
Matthias 2024-08-31 16:03:49 +02:00 committed by GitHub
commit 01d10aebca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
125 changed files with 6040 additions and 1964 deletions

View File

@ -1,9 +1,14 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: docker - package-ecosystem: docker
directory: "/" directories:
- "/"
- "/docker"
schedule: schedule:
interval: daily interval: daily
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
open-pull-requests-limit: 10 open-pull-requests-limit: 10
- package-ecosystem: pip - package-ecosystem: pip

View File

@ -55,7 +55,7 @@ jobs:
- name: Installation - *nix - name: Installation - *nix
run: | run: |
python -m pip install --upgrade "pip<=24.0" wheel python -m pip install --upgrade pip wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include export TA_INCLUDE_PATH=${HOME}/dependencies/include
@ -197,7 +197,7 @@ jobs:
- name: Installation (python) - name: Installation (python)
run: | run: |
python -m pip install --upgrade "pip<=24.0" wheel python -m pip install --upgrade pip wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include export TA_INCLUDE_PATH=${HOME}/dependencies/include
@ -384,7 +384,6 @@ jobs:
- name: Documentation build - name: Documentation build
run: | run: |
pip install -r docs/requirements-docs.txt pip install -r docs/requirements-docs.txt
pip install mkdocs
mkdocs build mkdocs build
- name: Discord notification - name: Discord notification
@ -427,7 +426,7 @@ jobs:
- name: Installation - *nix - name: Installation - *nix
run: | run: |
python -m pip install --upgrade "pip<=24.0" wheel python -m pip install --upgrade pip wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include export TA_INCLUDE_PATH=${HOME}/dependencies/include

55
.github/workflows/deploy-docs.yml vendored Normal file
View File

@ -0,0 +1,55 @@
name: Build Documentation
on:
push:
branches:
- develop
release:
types: [published]
# disable permissions for all of the available permissions
permissions: {}
jobs:
build-docs:
permissions:
contents: write # for mike to push
name: Deploy Docs through mike
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r docs/requirements-docs.txt
- name: Fetch gh-pages branch
run: |
git fetch origin gh-pages --depth=1
- name: Configure Git user
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
- name: Build and push Mike
if: ${{ github.event_name == 'push' }}
run: |
mike deploy ${{ github.ref_name }} latest --push --update-aliases
- name: Build and push Mike - Release
if: ${{ github.event_name == 'release' }}
run: |
mike deploy ${{ github.ref_name }} stable --push --update-aliases
- name: Show mike versions
run: |
mike list

2
.gitignore vendored
View File

@ -114,3 +114,5 @@ target/
!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 !config_examples/config_freqai.example.json
docker-compose-*.yml

View File

@ -2,24 +2,24 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/pycqa/flake8 - repo: https://github.com/pycqa/flake8
rev: "7.1.0" rev: "7.1.1"
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [Flake8-pyproject] additional_dependencies: [Flake8-pyproject]
# stages: [push] # stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.11.0" rev: "v1.11.2"
hooks: hooks:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
additional_dependencies: additional_dependencies:
- types-cachetools==5.4.0.20240717 - types-cachetools==5.5.0.20240820
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.32.0.20240712 - types-requests==2.32.0.20240712
- types-tabulate==0.9.0.20240106 - types-tabulate==0.9.0.20240106
- types-python-dateutil==2.9.0.20240316 - types-python-dateutil==2.9.0.20240821
- SQLAlchemy==2.0.31 - SQLAlchemy==2.0.32
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
@ -31,7 +31,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.5.4' rev: 'v0.6.2'
hooks: hooks:
- id: ruff - id: ruff

View File

@ -1,4 +1,4 @@
FROM python:3.12.4-slim-bookworm as base FROM python:3.12.5-slim-bookworm as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8
@ -25,7 +25,7 @@ FROM base as python-deps
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ && apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
&& apt-get clean \ && apt-get clean \
&& pip install --upgrade "pip<=24.0" wheel && pip install --upgrade pip wheel
# Install TA-lib # Install TA-lib
COPY build_helpers/* /tmp/ COPY build_helpers/* /tmp/

View File

@ -86,41 +86,50 @@ For further (native) installation methods, please refer to the [Installation doc
``` ```
usage: freqtrade [-h] [-V] usage: freqtrade [-h] [-V]
{trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} {trade,create-userdir,new-config,show-config,new-strategy,download-data,convert-data,convert-trade-data,trades-to-ohlcv,list-data,backtesting,backtesting-show,backtesting-analysis,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-markets,list-pairs,list-strategies,list-freqaimodels,list-timeframes,show-trades,test-pairlist,convert-db,install-ui,plot-dataframe,plot-profit,webserver,strategy-updater,lookahead-analysis,recursive-analysis}
... ...
Free, open source crypto trading bot Free, open source crypto trading bot
positional arguments: positional arguments:
{trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} {trade,create-userdir,new-config,show-config,new-strategy,download-data,convert-data,convert-trade-data,trades-to-ohlcv,list-data,backtesting,backtesting-show,backtesting-analysis,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-markets,list-pairs,list-strategies,list-freqaimodels,list-timeframes,show-trades,test-pairlist,convert-db,install-ui,plot-dataframe,plot-profit,webserver,strategy-updater,lookahead-analysis,recursive-analysis}
trade Trade module. trade Trade module.
create-userdir Create user-data directory. create-userdir Create user-data directory.
new-config Create new config new-config Create new config
show-config Show resolved config
new-strategy Create new strategy new-strategy Create new strategy
download-data Download backtesting data. download-data Download backtesting data.
convert-data Convert candle (OHLCV) data from one format to convert-data Convert candle (OHLCV) data from one format to
another. another.
convert-trade-data Convert trade data from one format to another. convert-trade-data Convert trade data from one format to another.
trades-to-ohlcv Convert trade data to OHLCV data.
list-data List downloaded data. list-data List downloaded data.
backtesting Backtesting module. backtesting Backtesting module.
backtesting-show Show past Backtest results
backtesting-analysis
Backtest Analysis module.
edge Edge module. edge Edge module.
hyperopt Hyperopt module. hyperopt Hyperopt module.
hyperopt-list List Hyperopt results hyperopt-list List Hyperopt results
hyperopt-show Show details of Hyperopt results hyperopt-show Show details of Hyperopt results
list-exchanges Print available exchanges. list-exchanges Print available exchanges.
list-hyperopts Print available hyperopt classes.
list-markets Print markets on exchange. list-markets Print markets on exchange.
list-pairs Print pairs on exchange. list-pairs Print pairs on exchange.
list-strategies Print available strategies. list-strategies Print available strategies.
list-freqaimodels Print available freqAI models.
list-timeframes Print available timeframes for the exchange. list-timeframes Print available timeframes for the exchange.
show-trades Show trades. show-trades Show trades.
test-pairlist Test your pairlist configuration. test-pairlist Test your pairlist configuration.
convert-db Migrate database to different system
install-ui Install FreqUI install-ui Install FreqUI
plot-dataframe Plot candles with indicators. plot-dataframe Plot candles with indicators.
plot-profit Generate plot showing profits. plot-profit Generate plot showing profits.
webserver Webserver module. webserver Webserver module.
strategy-updater updates outdated strategy files to the current version
lookahead-analysis Check for potential look ahead bias.
recursive-analysis Check for potential recursive formula issue.
optional arguments: options:
-h, --help show this help message and exit -h, --help show this help message and exit
-V, --version show program's version number and exit -V, --version show program's version number and exit

View File

@ -1,6 +1,6 @@
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040 # vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
python -m pip install --upgrade "pip<=24.0" wheel python -m pip install --upgrade pip wheel
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"

View File

@ -9,11 +9,6 @@
], ],
"minimum": -1 "minimum": -1
}, },
"new_pairs_days": {
"description": "Download data of new pairs for given number of days",
"type": "integer",
"default": 30
},
"timeframe": { "timeframe": {
"description": "The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). \nUsually specified in the strategy and missing in the configuration.", "description": "The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). \nUsually specified in the strategy and missing in the configuration.",
"type": "string" "type": "string"
@ -562,6 +557,7 @@
"enum": [ "enum": [
"StaticPairList", "StaticPairList",
"VolumePairList", "VolumePairList",
"PercentChangePairList",
"ProducerPairList", "ProducerPairList",
"RemotePairList", "RemotePairList",
"MarketCapPairList", "MarketCapPairList",
@ -609,6 +605,10 @@
"type": "number", "type": "number",
"minimum": 0 "minimum": 0
}, },
"unlock_at": {
"description": "Time when trading will be unlocked regularly. Format: HH:MM",
"type": "string"
},
"trade_limit": { "trade_limit": {
"description": "Minimum number of trades required during lookback period.", "description": "Minimum number of trades required during lookback period.",
"type": "number", "type": "number",
@ -1064,7 +1064,7 @@
"default": {}, "default": {},
"properties": { "properties": {
"process_throttle_secs": { "process_throttle_secs": {
"description": "Throttle time in seconds for processing.", "description": "Minimum loop duration for one bot iteration in seconds.",
"type": "integer" "type": "integer"
}, },
"interval": { "interval": {
@ -1105,6 +1105,15 @@
"description": "Enable position adjustment. \nUsually specified in the strategy and missing in the configuration.", "description": "Enable position adjustment. \nUsually specified in the strategy and missing in the configuration.",
"type": "boolean" "type": "boolean"
}, },
"new_pairs_days": {
"description": "Download data of new pairs for given number of days",
"type": "integer",
"default": 30
},
"download_trades": {
"description": "Download trades data by default (instead of ohlcv data).",
"type": "boolean"
},
"max_entry_position_adjustment": { "max_entry_position_adjustment": {
"description": "Maximum entry position adjustment allowed. \nUsually specified in the strategy and missing in the configuration.", "description": "Maximum entry position adjustment allowed. \nUsually specified in the strategy and missing in the configuration.",
"type": [ "type": [
@ -1113,6 +1122,13 @@
], ],
"minimum": -1 "minimum": -1
}, },
"add_config_files": {
"description": "Additional configuration files to load.",
"type": "array",
"items": {
"type": "string"
}
},
"orderflow": { "orderflow": {
"description": "Settings related to order flow.", "description": "Settings related to order flow.",
"type": "object", "type": "object",
@ -1208,6 +1224,11 @@
}, },
"uniqueItems": true "uniqueItems": true
}, },
"log_responses": {
"description": "Log responses from the exchange.Useful/required to debug issues with order processing.",
"type": "boolean",
"default": false
},
"unknown_fee_rate": { "unknown_fee_rate": {
"description": "Fee rate for unknown markets.", "description": "Fee rate for unknown markets.",
"type": "number" "type": "number"

View File

@ -1,4 +1,5 @@
{ {
"$schema": "https://schema.freqtrade.io/schema.json",
"max_open_trades": 3, "max_open_trades": 3,
"stake_currency": "USDT", "stake_currency": "USDT",
"stake_amount": 0.05, "stake_amount": 0.05,

View File

@ -1,4 +1,5 @@
{ {
"$schema": "https://schema.freqtrade.io/schema.json",
"trading_mode": "futures", "trading_mode": "futures",
"margin_mode": "isolated", "margin_mode": "isolated",
"max_open_trades": 5, "max_open_trades": 5,

View File

@ -1,4 +1,5 @@
{ {
"$schema": "https://schema.freqtrade.io/schema.json",
"max_open_trades": 3, "max_open_trades": 3,
"stake_currency": "BTC", "stake_currency": "BTC",
"stake_amount": 0.05, "stake_amount": 0.05,

View File

@ -1,4 +1,5 @@
{ {
"$schema": "https://schema.freqtrade.io/schema.json",
"max_open_trades": 5, "max_open_trades": 5,
"stake_currency": "EUR", "stake_currency": "EUR",
"stake_amount": 10, "stake_amount": 10,

View File

@ -1,4 +1,4 @@
FROM python:3.11.8-slim-bookworm as base FROM python:3.11.9-slim-bookworm as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8
@ -17,7 +17,7 @@ RUN mkdir /freqtrade \
&& chown ftuser:ftuser /freqtrade \ && chown ftuser:ftuser /freqtrade \
# Allow sudoers # Allow sudoers
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \ && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \
&& pip install --upgrade "pip<=24.0" && pip install --upgrade pip
WORKDIR /freqtrade WORKDIR /freqtrade

View File

@ -30,11 +30,17 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
""" """
@staticmethod @staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int, def hyperopt_loss_function(
min_date: datetime, max_date: datetime, *,
config: Config, processed: Dict[str, DataFrame], results: DataFrame,
backtest_stats: Dict[str, Any], trade_count: int,
*args, **kwargs) -> float: min_date: datetime,
max_date: datetime,
config: Config,
processed: Dict[str, DataFrame],
backtest_stats: Dict[str, Any],
**kwargs,
) -> float:
""" """
Objective function, returns smaller number for better results Objective function, returns smaller number for better results
This is the legacy algorithm (used until now in freqtrade). This is the legacy algorithm (used until now in freqtrade).

View File

@ -530,10 +530,10 @@ 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 - Exchange [trading limits](#trading-limits-in-backtesting) are respected
- Entries happen at open-price - Entries happen at open-price unless a custom price logic has been specified
- All orders are filled at the requested price (no slippage) as long as the price is within the candle's high/low range - All orders are filled at the requested price (no slippage) as long as the price is within the candle's high/low range
- Exit-signal exits happen at open-price of the consecutive candle - Exit-signal exits happen at open-price of the consecutive candle
- Exits don't free their trade slot for a new trade until the next candle - Exits free their trade slot for a new trade with a different pair
- Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open - Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open
- ROI - ROI
- Exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%) - Exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%)

View File

@ -12,41 +12,50 @@ This page explains the different parameters of the bot and how to run it.
``` ```
usage: freqtrade [-h] [-V] usage: freqtrade [-h] [-V]
{trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} {trade,create-userdir,new-config,show-config,new-strategy,download-data,convert-data,convert-trade-data,trades-to-ohlcv,list-data,backtesting,backtesting-show,backtesting-analysis,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-markets,list-pairs,list-strategies,list-freqaimodels,list-timeframes,show-trades,test-pairlist,convert-db,install-ui,plot-dataframe,plot-profit,webserver,strategy-updater,lookahead-analysis,recursive-analysis}
... ...
Free, open source crypto trading bot Free, open source crypto trading bot
positional arguments: positional arguments:
{trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} {trade,create-userdir,new-config,show-config,new-strategy,download-data,convert-data,convert-trade-data,trades-to-ohlcv,list-data,backtesting,backtesting-show,backtesting-analysis,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-markets,list-pairs,list-strategies,list-freqaimodels,list-timeframes,show-trades,test-pairlist,convert-db,install-ui,plot-dataframe,plot-profit,webserver,strategy-updater,lookahead-analysis,recursive-analysis}
trade Trade module. trade Trade module.
create-userdir Create user-data directory. create-userdir Create user-data directory.
new-config Create new config new-config Create new config
show-config Show resolved config
new-strategy Create new strategy new-strategy Create new strategy
download-data Download backtesting data. download-data Download backtesting data.
convert-data Convert candle (OHLCV) data from one format to convert-data Convert candle (OHLCV) data from one format to
another. another.
convert-trade-data Convert trade data from one format to another. convert-trade-data Convert trade data from one format to another.
trades-to-ohlcv Convert trade data to OHLCV data.
list-data List downloaded data. list-data List downloaded data.
backtesting Backtesting module. backtesting Backtesting module.
backtesting-show Show past Backtest results
backtesting-analysis
Backtest Analysis module.
edge Edge module. edge Edge module.
hyperopt Hyperopt module. hyperopt Hyperopt module.
hyperopt-list List Hyperopt results hyperopt-list List Hyperopt results
hyperopt-show Show details of Hyperopt results hyperopt-show Show details of Hyperopt results
list-exchanges Print available exchanges. list-exchanges Print available exchanges.
list-hyperopts Print available hyperopt classes.
list-markets Print markets on exchange. list-markets Print markets on exchange.
list-pairs Print pairs on exchange. list-pairs Print pairs on exchange.
list-strategies Print available strategies. list-strategies Print available strategies.
list-freqaimodels Print available freqAI models.
list-timeframes Print available timeframes for the exchange. list-timeframes Print available timeframes for the exchange.
show-trades Show trades. show-trades Show trades.
test-pairlist Test your pairlist configuration. test-pairlist Test your pairlist configuration.
convert-db Migrate database to different system
install-ui Install FreqUI install-ui Install FreqUI
plot-dataframe Plot candles with indicators. plot-dataframe Plot candles with indicators.
plot-profit Generate plot showing profits. plot-profit Generate plot showing profits.
webserver Webserver module. webserver Webserver module.
strategy-updater updates outdated strategy files to the current version
lookahead-analysis Check for potential look ahead bias.
recursive-analysis Check for potential recursive formula issue.
optional arguments: options:
-h, --help show this help message and exit -h, --help show this help message and exit
-V, --version show program's version number and exit -V, --version show program's version number and exit

View File

@ -123,6 +123,19 @@ This is similar to using multiple `--config` parameters, but simpler in usage as
If multiple files are in the `add_config_files` section, then they will be assumed to be at identical levels, having the last occurrence override the earlier config (unless a parent already defined such a key). If multiple files are in the `add_config_files` section, then they will be assumed to be at identical levels, having the last occurrence override the earlier config (unless a parent already defined such a key).
## Editor autocomplete and validation
If you are using an editor that supports JSON schema, you can use the schema provided by Freqtrade to get autocompletion and validation of your configuration file by adding the following line to the top of your configuration file:
``` json
{
"$schema": "https://schema.freqtrade.io/schema.json",
}
```
??? Note "Develop version"
The develop schema is available as `https://schema.freqtrade.io/schema_dev.json` - though we recommend to stick to the stable version for the best experience.
## Configuration parameters ## Configuration parameters
The table below will list all configuration parameters available. The table below will list all configuration parameters available.

View File

@ -423,7 +423,8 @@ You can get a list of downloaded data using the `list-data` sub-command.
usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [--exchange EXCHANGE] [--userdir PATH] [--exchange EXCHANGE]
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}] [--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
[-p PAIRS [PAIRS ...]] [--data-format-trades {json,jsongz,hdf5,feather,parquet}]
[--trades] [-p PAIRS [PAIRS ...]]
[--trading-mode {spot,margin,futures}] [--trading-mode {spot,margin,futures}]
[--show-timerange] [--show-timerange]
@ -433,6 +434,10 @@ options:
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet} --data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
Storage format for downloaded candle (OHLCV) data. Storage format for downloaded candle (OHLCV) data.
(default: `feather`). (default: `feather`).
--data-format-trades {json,jsongz,hdf5,feather,parquet}
Storage format for downloaded trades data. (default:
`feather`).
--trades Work on trades data instead of OHLCV data.
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Limit command to these pairs. Pairs are space- Limit command to these pairs. Pairs are space-
separated. separated.
@ -465,13 +470,29 @@ Common arguments:
```bash ```bash
> freqtrade list-data --userdir ~/.freqtrade/user_data/ > freqtrade list-data --userdir ~/.freqtrade/user_data/
Found 33 pair / timeframe combinations. Found 33 pair / timeframe combinations.
pairs timeframe ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━┓
---------- ----------------------------------------- ┃ Pair ┃ Timeframe ┃ Type ┃
ADA/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d ┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━┩
ADA/ETH 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d │ ADA/BTC │ 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d │ spot │
ETH/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d │ ADA/ETH │ 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d │ spot │
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h │ ETH/BTC │ 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d │ spot │
│ ETH/USDT │ 5m, 15m, 30m, 1h, 2h, 4h │ spot │
└───────────────┴───────────────────────────────────────────┴──────┘
```
Show all trades data including from/to timerange
``` bash
> freqtrade list-data --show --trades
Found trades data for 1 pair.
┏━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓
┃ Pair ┃ Type ┃ From ┃ To ┃ Trades ┃
┡━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩
│ XRP/ETH │ spot │ 2019-10-11 00:00:11 │ 2019-10-13 11:19:28 │ 12477 │
└─────────┴──────┴─────────────────────┴─────────────────────┴────────┘
``` ```
## Trades (tick) data ## Trades (tick) data

View File

@ -2,11 +2,11 @@
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings. Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler). In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) and [`PercentChangePairList`](#percent-change-pair-list) Pairlist Handlers).
Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList` or `MarketCapPairList` as the starting Pairlist Handler. If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList`, `MarketCapPairList` or `PercentChangePairList` as the starting Pairlist Handler.
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist. Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
@ -22,6 +22,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
* [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`StaticPairList`](#static-pair-list) (default, if not configured differently)
* [`VolumePairList`](#volume-pair-list) * [`VolumePairList`](#volume-pair-list)
* [`PercentChangePairList`](#percent-change-pair-list)
* [`ProducerPairList`](#producerpairlist) * [`ProducerPairList`](#producerpairlist)
* [`RemotePairList`](#remotepairlist) * [`RemotePairList`](#remotepairlist)
* [`MarketCapPairList`](#marketcappairlist) * [`MarketCapPairList`](#marketcappairlist)
@ -152,6 +153,89 @@ More sophisticated approach can be used, by using `lookback_timeframe` for candl
!!! Note !!! Note
`VolumePairList` does not support backtesting mode. `VolumePairList` does not support backtesting mode.
#### Percent Change Pair List
`PercentChangePairList` filters and sorts pairs based on the percentage change in their price over the last 24 hours or any defined timeframe as part of advanced options. This allows traders to focus on assets that have experienced significant price movements, either positive or negative.
**Configuration Options**
* `number_assets`: Specifies the number of top pairs to select based on the 24-hour percentage change.
* `min_value`: Sets a minimum percentage change threshold. Pairs with a percentage change below this value will be filtered out.
* `max_value`: Sets a maximum percentage change threshold. Pairs with a percentage change above this value will be filtered out.
* `sort_direction`: Specifies the order in which pairs are sorted based on their percentage change. Accepts two values: `asc` for ascending order and `desc` for descending order.
* `refresh_period`: Defines the interval (in seconds) at which the pairlist will be refreshed. The default is 1800 seconds (30 minutes).
* `lookback_days`: Number of days to look back. When `lookback_days` is selected, the `lookback_timeframe` is defaulted to 1 day.
* `lookback_timeframe`: Timeframe to use for the lookback period.
* `lookback_period`: Number of periods to look back at.
When PercentChangePairList is used after other Pairlist Handlers, it will operate on the outputs of those handlers. If it is the leading Pairlist Handler, it will select pairs from all available markets with the specified stake currency.
`PercentChangePairList` uses ticker data from the exchange, provided via the ccxt library:
The percentage change is calculated as the change in price over the last 24 hours.
??? Note "Unsupported exchanges"
On some exchanges (like HTX), regular PercentChangePairList does not work as the api does not natively provide 24h percent change in price. This can be worked around by using candle data to calculate the percentage change. To roughly simulate 24h percent change, you can use the following configuration. Please note that these pairlists will only refresh once per day.
```json
"pairlists": [
{
"method": "PercentChangePairList",
"number_assets": 20,
"min_value": 0,
"refresh_period": 86400,
"lookback_days": 1
}
],
```
**Example Configuration to Read from Ticker**
```json
"pairlists": [
{
"method": "PercentChangePairList",
"number_assets": 15,
"min_value": -10,
"max_value": 50
}
],
```
In this configuration:
1. The top 15 pairs are selected based on the highest percentage change in price over the last 24 hours.
2. Only pairs with a percentage change between -10% and 50% are considered.
**Example Configuration to Read from Candles**
```json
"pairlists": [
{
"method": "PercentChangePairList",
"number_assets": 15,
"sort_key": "percentage",
"min_value": 0,
"refresh_period": 3600,
"lookback_timeframe": "1h",
"lookback_period": 72
}
],
```
This example builds the percent change pairs based on a rolling period of 3 days of 1-hour candles by using `lookback_timeframe` for candle size and `lookback_period` which specifies the number of candles.
The percent change in price is calculated using the following formula, which expresses the percentage difference between the current candle's close price and the previous candle's close price, as defined by the specified timeframe and lookback period:
$$ Percent Change = (\frac{Current Close - Previous Close}{Previous Close}) * 100 $$
!!! Warning "Range look back and refresh period"
When used in conjunction with `lookback_days` and `lookback_timeframe` the `refresh_period` can not be smaller than the candle size in seconds. As this will result in unnecessary requests to the exchanges API.
!!! Warning "Performance implications when using lookback range"
If used in first position in combination with lookback, the computation of the range-based percent change can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `PercentChangePairList` to narrow the pairlist down for further percent-change calculation.
!!! Note "Backtesting"
`PercentChangePairList` does not support backtesting mode.
#### ProducerPairList #### ProducerPairList
With `ProducerPairList`, you can reuse the pairlist from a [Producer](producer-consumer.md) without explicitly defining the pairlist on each consumer. With `ProducerPairList`, you can reuse the pairlist from a [Producer](producer-consumer.md) without explicitly defining the pairlist on each consumer.

View File

@ -36,6 +36,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections. <br> **Datatype:** Positive integer (in candles). | `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections. <br> **Datatype:** Positive integer (in candles).
| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered. <br>Cannot be used together with `lookback_period_candles`. <br>This setting may be ignored by some Protections. <br> **Datatype:** Float (in minutes) | `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered. <br>Cannot be used together with `lookback_period_candles`. <br>This setting may be ignored by some Protections. <br> **Datatype:** Float (in minutes)
| `trade_limit` | Number of trades required at minimum (not used by all Protections). <br> **Datatype:** Positive integer | `trade_limit` | Number of trades required at minimum (not used by all Protections). <br> **Datatype:** Positive integer
| `unlock_at` | Time when trading will be unlocked regularly (not used by all Protections). <br> **Datatype:** string <br>**Input Format:** "HH:MM" (24-hours)
!!! Note "Durations" !!! Note "Durations"
Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles). Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles).
@ -44,7 +45,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
#### Stoploss Guard #### Stoploss Guard
`StoplossGuard` selects all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`). `StoplossGuard` selects all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`).
If `trade_limit` or more trades resulted in stoploss, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`). If `trade_limit` or more trades resulted in stoploss, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`, or until the set time when using `unlock_at`).
This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time.
@ -97,7 +98,7 @@ def protections(self):
#### Low Profit Pairs #### Low Profit Pairs
`LowProfitPairs` uses all trades for a pair within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the overall profit ratio. `LowProfitPairs` uses all trades for a pair within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the overall profit ratio.
If that ratio is below `required_profit`, that pair will be locked for `stop_duration` in minutes (or in candles when using `stop_duration_candles`). If that ratio is below `required_profit`, that pair will be locked for `stop_duration` in minutes (or in candles when using `stop_duration_candles`, or until the set time when using `unlock_at`).
For futures bots, setting `only_per_side` will make the bot only consider one side, and will then only lock this one side, allowing for example shorts to continue after a series of long losses. For futures bots, setting `only_per_side` will make the bot only consider one side, and will then only lock this one side, allowing for example shorts to continue after a series of long losses.
@ -120,7 +121,7 @@ def protections(self):
#### Cooldown Period #### Cooldown Period
`CooldownPeriod` locks a pair for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after selling, avoiding a re-entry for this pair for `stop_duration` minutes. `CooldownPeriod` locks a pair for `stop_duration` in minutes (or in candles when using `stop_duration_candles`, or until the set time when using `unlock_at`) after exiting, avoiding a re-entry for this pair for `stop_duration` minutes.
The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down". The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down".

View File

@ -0,0 +1,45 @@
## Imports necessary for a strategy
When creating a strategy, you will need to import the necessary modules and classes. The following imports are required for a strategy:
By default, we recommend the following imports as a base line for your strategy:
This will cover all imports necessary for freqtrade functions to work.
Obviously you can add more imports as needed for your strategy.
``` python
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these imports ---
import numpy as np
import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame
from typing import Dict, Optional, Union, Tuple
from freqtrade.strategy import (
IStrategy,
Trade,
Order,
PairLocks,
informative, # @informative decorator
# Hyperopt Parameters
BooleanParameter,
CategoricalParameter,
DecimalParameter,
IntParameter,
RealParameter,
# timeframe helpers
timeframe_to_minutes,
timeframe_to_next_date,
timeframe_to_prev_date,
# Strategy helper functions
merge_informative_pair,
stoploss_from_absolute,
stoploss_from_open,
)
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
from technical import qtpylib
```

View File

@ -1,6 +1,7 @@
markdown==3.6 markdown==3.7
mkdocs==1.6.0 mkdocs==1.6.0
mkdocs-material==9.5.29 mkdocs-material==9.5.33
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==10.8.1 pymdown-extensions==10.9
jinja2==3.1.4 jinja2==3.1.4
mike==2.1.3

View File

@ -24,6 +24,8 @@ Currently available callbacks:
!!! Tip "Callback calling sequence" !!! Tip "Callback calling sequence"
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic) You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
--8<-- "includes/strategy-imports.md"
## Bot start ## Bot start
A simple callback which is called once when the strategy is loaded. A simple callback which is called once when the strategy is loaded.
@ -41,10 +43,10 @@ class AwesomeStrategy(IStrategy):
Called only once after bot instantiation. Called only once after bot instantiation.
: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.
""" """
if self.config['runmode'].value in ('live', 'dry_run'): if self.config["runmode"].value in ("live", "dry_run"):
# Assign this to the class by using self.* # Assign this to the class by using self.*
# can then be used by populate_* methods # can then be used by populate_* methods
self.custom_remote_data = requests.get('https://some_remote_source.example.com') self.custom_remote_data = requests.get("https://some_remote_source.example.com")
``` ```
@ -57,6 +59,7 @@ seconds, unless configured differently) or once per candle in backtest/hyperopt
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc. This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
``` python ``` python
# Default imports
import requests import requests
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -71,10 +74,10 @@ class AwesomeStrategy(IStrategy):
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
: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.
""" """
if self.config['runmode'].value in ('live', 'dry_run'): if self.config["runmode"].value in ("live", "dry_run"):
# Assign this to the class by using self.* # Assign this to the class by using self.*
# can then be used by populate_* methods # can then be used by populate_* methods
self.remote_data = requests.get('https://some_remote_source.example.com') self.remote_data = requests.get("https://some_remote_source.example.com")
``` ```
@ -83,6 +86,8 @@ class AwesomeStrategy(IStrategy):
Called before entering a trade, makes it possible to manage your position size when placing a new trade. Called before entering a trade, makes it possible to manage your position size when placing a new trade.
```python ```python
# Default imports
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
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,
@ -92,13 +97,13 @@ class AwesomeStrategy(IStrategy):
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
current_candle = dataframe.iloc[-1].squeeze() current_candle = dataframe.iloc[-1].squeeze()
if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']: if current_candle["fastk_rsi_1h"] > current_candle["fastd_rsi_1h"]:
if self.config['stake_amount'] == 'unlimited': if self.config["stake_amount"] == "unlimited":
# Use entire available wallet during favorable conditions when in compounding mode. # Use entire available wallet during favorable conditions when in compounding mode.
return max_stake return max_stake
else: else:
# Compound profits during favorable conditions instead of using a static stake. # Compound profits during favorable conditions instead of using a static stake.
return self.wallets.get_total_stake_amount() / self.config['max_open_trades'] return self.wallets.get_total_stake_amount() / self.config["max_open_trades"]
# Use default stake amount. # Use default stake amount.
return proposed_stake return proposed_stake
@ -129,25 +134,27 @@ Using `custom_exit()` signals in place of stoploss though *is not recommended*.
An example of how we can use different indicators depending on the current profit and also exit trades that were open longer than one day: An example of how we can use different indicators depending on the current profit and also exit trades that were open longer than one day:
``` python ``` python
# Default imports
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs): current_profit: float, **kwargs):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze() last_candle = dataframe.iloc[-1].squeeze()
# Above 20% profit, sell when rsi < 80 # Above 20% profit, sell when rsi < 80
if current_profit > 0.2: if current_profit > 0.2:
if last_candle['rsi'] < 80: if last_candle["rsi"] < 80:
return 'rsi_below_80' return "rsi_below_80"
# Between 2% and 10%, sell if EMA-long above EMA-short # Between 2% and 10%, sell if EMA-long above EMA-short
if 0.02 < current_profit < 0.1: if 0.02 < current_profit < 0.1:
if last_candle['emalong'] > last_candle['emashort']: if last_candle["emalong"] > last_candle["emashort"]:
return 'ema_long_below_80' return "ema_long_below_80"
# Sell any positions at a loss if they are held for more than one day. # Sell any positions at a loss if they are held for more than one day.
if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1: if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1:
return 'unclog' return "unclog"
``` ```
See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks. See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks.
@ -168,7 +175,6 @@ The absolute value of the return value is used (the sign is ignored), so returni
Returning `None` will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss. Returning `None` will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss.
`NaN` and `inf` values are considered invalid and will be ignored (identical to `None`). `NaN` and `inf` values are considered invalid and will be ignored (identical to `None`).
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchangefreqtrade)). Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchangefreqtrade)).
!!! Note "Use of dates" !!! Note "Use of dates"
@ -196,9 +202,7 @@ Of course, many more things are possible, and all examples can be combined at wi
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method: To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
``` python ``` python
# additional imports required # Default imports
from datetime import datetime
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -206,7 +210,7 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool, current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
""" """
@ -236,8 +240,7 @@ class AwesomeStrategy(IStrategy):
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
``` python ``` python
from datetime import datetime, timedelta # Default imports
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -245,7 +248,7 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool, current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
@ -263,8 +266,7 @@ Use the initial stoploss for the first 60 minutes, after this change to 10% trai
If an additional order fills, set stoploss to -10% below the new `open_rate` ([Averaged across all entries](#position-adjust-calculations)). If an additional order fills, set stoploss to -10% below the new `open_rate` ([Averaged across all entries](#position-adjust-calculations)).
``` python ``` python
from datetime import datetime, timedelta # Default imports
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -272,7 +274,7 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool, current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
@ -293,8 +295,7 @@ Use a different stoploss depending on the pair.
In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs. In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs.
``` python ``` python
from datetime import datetime # Default imports
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -302,13 +303,13 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool, current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
if pair in ('ETH/BTC', 'XRP/BTC'): if pair in ("ETH/BTC", "XRP/BTC"):
return -0.10 return -0.10
elif pair in ('LTC/BTC'): elif pair in ("LTC/BTC"):
return -0.05 return -0.05
return -0.15 return -0.15
``` ```
@ -320,8 +321,7 @@ Use the initial stoploss until the profit is above 4%, then use a trailing stopl
Please note that the stoploss can only increase, values lower than the current stoploss are ignored. Please note that the stoploss can only increase, values lower than the current stoploss are ignored.
``` python ``` python
from datetime import datetime, timedelta # Default imports
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -329,7 +329,7 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool, current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
@ -353,9 +353,7 @@ Instead of continuously trailing behind the current price, this example sets fix
* Once profit is > 40% - set stoploss to 25% above open price. * Once profit is > 40% - set stoploss to 25% above open price.
``` python ``` python
from datetime import datetime # Default imports
from freqtrade.persistence import Trade
from freqtrade.strategy import stoploss_from_open
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -363,7 +361,7 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool, current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
@ -384,15 +382,17 @@ class AwesomeStrategy(IStrategy):
Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss. Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss.
``` python ``` python
# Default imports
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# <...> # <...>
dataframe['sar'] = ta.SAR(dataframe) dataframe["sar"] = ta.SAR(dataframe)
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool, current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
@ -400,7 +400,7 @@ class AwesomeStrategy(IStrategy):
last_candle = dataframe.iloc[-1].squeeze() last_candle = dataframe.iloc[-1].squeeze()
# Use parabolic sar as absolute stoploss price # Use parabolic sar as absolute stoploss price
stoploss_price = last_candle['sar'] stoploss_price = last_candle["sar"]
# Convert absolute price to percentage relative to current_rate # Convert absolute price to percentage relative to current_rate
if stoploss_price < current_rate: if stoploss_price < current_rate:
@ -429,10 +429,7 @@ Stoploss values returned from `custom_stoploss()` must specify a percentage rela
``` python ``` python
# Default imports
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, stoploss_from_open
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -440,7 +437,7 @@ Stoploss values returned from `custom_stoploss()` must specify a percentage rela
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool, current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
@ -469,38 +466,34 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
??? Example "Returning a stoploss using absolute price from the custom stoploss function" ??? Example "Returning a stoploss using absolute price from the custom stoploss function"
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), current_rate=current_rate, is_short=trade.is_short, leverage=trade.leverage)`. If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate + (side * candle["atr"] * 2), current_rate=current_rate, is_short=trade.is_short, leverage=trade.leverage)`.
For futures, we need to adjust the direction (up or down), as well as adjust for leverage, since the [`custom_stoploss`](strategy-callbacks.md#custom-stoploss) callback returns the ["risk for this trade"](stoploss.md#stoploss-and-leverage) - not the relative price movement. For futures, we need to adjust the direction (up or down), as well as adjust for leverage, since the [`custom_stoploss`](strategy-callbacks.md#custom-stoploss) callback returns the ["risk for this trade"](stoploss.md#stoploss-and-leverage) - not the relative price movement.
``` python ``` python
# Default imports
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, stoploss_from_absolute, timeframe_to_prev_date
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
return dataframe return dataframe
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool, current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
candle = dataframe.iloc[-1].squeeze() candle = dataframe.iloc[-1].squeeze()
side = 1 if trade.is_short else -1 side = 1 if trade.is_short else -1
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), return stoploss_from_absolute(current_rate + (side * candle["atr"] * 2),
current_rate=current_rate, current_rate=current_rate,
is_short=trade.is_short, is_short=trade.is_short,
leverage=trade.leverage) leverage=trade.leverage)
``` ```
--- ---
## Custom order price rules ## Custom order price rules
@ -520,19 +513,18 @@ Each of these methods are called right before placing an order on the exchange.
### Custom order entry and exit price example ### Custom order entry and exit price example
``` python ``` python
from datetime import datetime, timedelta, timezone # Default imports
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
def custom_entry_price(self, pair: str, trade: Optional['Trade'], current_time: datetime, proposed_rate: float, def custom_entry_price(self, pair: str, trade: Optional[Trade], current_time: datetime, proposed_rate: float,
entry_tag: Optional[str], side: str, **kwargs) -> float: entry_tag: Optional[str], side: str, **kwargs) -> float:
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
timeframe=self.timeframe) timeframe=self.timeframe)
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1] new_entryprice = dataframe["bollinger_10_lowerband"].iat[-1]
return new_entryprice return new_entryprice
@ -542,7 +534,7 @@ class AwesomeStrategy(IStrategy):
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
timeframe=self.timeframe) timeframe=self.timeframe)
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1] new_exitprice = dataframe["bollinger_10_upperband"].iat[-1]
return new_exitprice return new_exitprice
@ -579,8 +571,7 @@ It applies a tight timeout for higher priced assets, while allowing more time to
The function must return either `True` (cancel order) or `False` (keep order alive). The function must return either `True` (cancel order) or `False` (keep order alive).
``` python ``` python
from datetime import datetime, timedelta # Default imports
from freqtrade.persistence import Trade, Order
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -588,11 +579,11 @@ class AwesomeStrategy(IStrategy):
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours. # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
unfilledtimeout = { unfilledtimeout = {
'entry': 60 * 25, "entry": 60 * 25,
'exit': 60 * 25 "exit": 60 * 25
} }
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order', def check_entry_timeout(self, pair: str, trade: Trade, order: Order,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5): if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True return True
@ -603,7 +594,7 @@ class AwesomeStrategy(IStrategy):
return False return False
def check_exit_timeout(self, pair: str, trade: Trade, order: 'Order', def check_exit_timeout(self, pair: str, trade: Trade, order: Order,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5): if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True return True
@ -620,8 +611,7 @@ class AwesomeStrategy(IStrategy):
### Custom order timeout example (using additional data) ### Custom order timeout example (using additional data)
``` python ``` python
from datetime import datetime # Default imports
from freqtrade.persistence import Trade, Order
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -629,24 +619,24 @@ class AwesomeStrategy(IStrategy):
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours. # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
unfilledtimeout = { unfilledtimeout = {
'entry': 60 * 25, "entry": 60 * 25,
'exit': 60 * 25 "exit": 60 * 25
} }
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order', def check_entry_timeout(self, pair: str, trade: Trade, order: Order,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
ob = self.dp.orderbook(pair, 1) ob = self.dp.orderbook(pair, 1)
current_price = ob['bids'][0][0] current_price = ob["bids"][0][0]
# Cancel buy order if price is more than 2% above the order. # Cancel buy order if price is more than 2% above the order.
if current_price > order.price * 1.02: if current_price > order.price * 1.02:
return True return True
return False return False
def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order', def check_exit_timeout(self, pair: str, trade: Trade, order: Order,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
ob = self.dp.orderbook(pair, 1) ob = self.dp.orderbook(pair, 1)
current_price = ob['asks'][0][0] current_price = ob["asks"][0][0]
# Cancel sell order if price is more than 2% below the order. # Cancel sell order if price is more than 2% below the order.
if current_price < order.price * 0.98: if current_price < order.price * 0.98:
return True return True
@ -665,6 +655,8 @@ This are the last methods that will be called before an order is placed.
`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect). `confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect).
``` python ``` python
# Default imports
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
@ -689,7 +681,7 @@ class AwesomeStrategy(IStrategy):
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade :param side: "long" or "short" - indicating the direction of the proposed trade
: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 bool: When True is returned, then the buy-order is placed on the exchange. :return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process False aborts the process
@ -711,8 +703,7 @@ The exit-reasons (if applicable) will be in the following sequence:
* `trailing_stop_loss` * `trailing_stop_loss`
``` python ``` python
from freqtrade.persistence import Trade # Default imports
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -738,14 +729,14 @@ class AwesomeStrategy(IStrategy):
or current rate for market orders. or current rate for market orders.
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param exit_reason: Exit reason. :param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', Can be any of ["roi", "stop_loss", "stoploss_on_exchange", "trailing_stop_loss",
'exit_signal', 'force_exit', 'emergency_exit'] "exit_signal", "force_exit", "emergency_exit"]
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
: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 bool: When True, then the exit-order is placed on the exchange. :return bool: When True, then the exit-order is placed on the exchange.
False aborts the process False aborts the process
""" """
if exit_reason == 'force_exit' and trade.calc_profit_ratio(rate) < 0: if exit_reason == "force_exit" and trade.calc_profit_ratio(rate) < 0:
# Reject force-sells with negative profit # Reject force-sells with negative profit
# This is just a sample, please adjust to your needs # This is just a sample, please adjust to your needs
# (this does not necessarily make sense, assuming you know when you're force-selling) # (this does not necessarily make sense, assuming you know when you're force-selling)
@ -771,7 +762,7 @@ This callback is **not** called when there is an open order (either buy or sell)
`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.
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. 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.
Adjustment orders can be assigned with a tag by returning a 2 element Tuple, with the first element being the adjustment amount, and the 2nd element the tag (e.g. `return 250, 'increase_favorable_conditions'`). Adjustment orders can be assigned with a tag by returning a 2 element Tuple, with the first element being the adjustment amount, and the 2nd element the tag (e.g. `return 250, "increase_favorable_conditions"`).
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage. Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
@ -793,7 +784,7 @@ Returning a value more than the above (so remaining stake_amount would become ne
!!! 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.
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that. If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
Using 'unlimited' stake amount with DCA orders requires you to also implement the `custom_stake_amount()` callback to avoid allocating all funds to the initial order. Using `"unlimited"` stake amount with DCA orders requires you to also implement the `custom_stake_amount()` callback to avoid allocating all funds to the initial order.
!!! Warning "Stoploss calculation" !!! Warning "Stoploss calculation"
Stoploss is still calculated from the initial opening price, not averaged price. Stoploss is still calculated from the initial opening price, not averaged price.
@ -811,9 +802,7 @@ Returning a value more than the above (so remaining stake_amount would become ne
Trades with long duration and 10s or even 100ds of position adjustments are therefore not recommended, and should be closed at regular intervals to not affect performance. Trades with long duration and 10s or even 100ds of position adjustments are therefore not recommended, and should be closed at regular intervals to not affect performance.
``` python ``` python
from freqtrade.persistence import Trade # Default imports
from typing import Optional, Tuple, Union
class DigDeeperStrategy(IStrategy): class DigDeeperStrategy(IStrategy):
@ -876,7 +865,7 @@ class DigDeeperStrategy(IStrategy):
if current_profit > 0.05 and trade.nr_of_successful_exits == 0: if current_profit > 0.05 and trade.nr_of_successful_exits == 0:
# Take half of the profit at +5% # Take half of the profit at +5%
return -(trade.stake_amount / 2), 'half_profit_5%' return -(trade.stake_amount / 2), "half_profit_5%"
if current_profit > -0.05: if current_profit > -0.05:
return None return None
@ -886,7 +875,7 @@ class DigDeeperStrategy(IStrategy):
# Only buy when not actively falling price. # Only buy when not actively falling price.
last_candle = dataframe.iloc[-1].squeeze() last_candle = dataframe.iloc[-1].squeeze()
previous_candle = dataframe.iloc[-2].squeeze() previous_candle = dataframe.iloc[-2].squeeze()
if last_candle['close'] < previous_candle['close']: if last_candle["close"] < previous_candle["close"]:
return None return None
filled_entries = trade.select_filled_orders(trade.entry_side) filled_entries = trade.select_filled_orders(trade.entry_side)
@ -904,7 +893,7 @@ class DigDeeperStrategy(IStrategy):
stake_amount = filled_entries[0].stake_amount stake_amount = filled_entries[0].stake_amount
# This then calculates current safety order size # This then calculates current safety order size
stake_amount = stake_amount * (1 + (count_of_entries * 0.25)) stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
return stake_amount, '1/3rd_increase' return stake_amount, "1/3rd_increase"
except Exception as exception: except Exception as exception:
return None return None
@ -951,8 +940,7 @@ If the cancellation of the original order fails, then the order will not be repl
Entry Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations. Entry Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations.
```python ```python
from freqtrade.persistence import Trade # Default imports
from datetime import timedelta, datetime
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -977,13 +965,18 @@ class AwesomeStrategy(IStrategy):
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing. :param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
:param current_order_rate: Rate of the existing order in place. :param current_order_rate: Rate of the existing order in place.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade :param side: "long" or "short" - indicating the direction of the proposed trade
: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: New entry price value if provided :return float: New entry price value if provided
""" """
# Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair. # Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
if pair == 'BTC/USDT' and entry_tag == 'long_sma200' and side == 'long' and (current_time - timedelta(minutes=10)) > trade.open_date_utc: if (
pair == "BTC/USDT"
and entry_tag == "long_sma200"
and side == "long"
and (current_time - timedelta(minutes=10)) > trade.open_date_utc
):
# just cancel the order if it has been filled more than half of the amount # just cancel the order if it has been filled more than half of the amount
if order.filled > order.remaining: if order.filled > order.remaining:
return None return None
@ -991,7 +984,7 @@ class AwesomeStrategy(IStrategy):
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
current_candle = dataframe.iloc[-1].squeeze() current_candle = dataframe.iloc[-1].squeeze()
# desired price # desired price
return current_candle['sma_200'] return current_candle["sma_200"]
# default: maintain existing order # default: maintain existing order
return current_order_rate return current_order_rate
``` ```
@ -1006,6 +999,8 @@ Values that are above `max_leverage` will be adjusted to `max_leverage`.
For markets / exchanges that don't support leverage, this method is ignored. For markets / exchanges that don't support leverage, this method is ignored.
``` python ``` python
# Default imports
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
def leverage(self, pair: str, current_time: datetime, current_rate: float, def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str, proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
@ -1019,7 +1014,7 @@ class AwesomeStrategy(IStrategy):
:param proposed_leverage: A leverage proposed by the bot. :param proposed_leverage: A leverage proposed by the bot.
:param max_leverage: Max leverage allowed on this pair :param max_leverage: Max leverage allowed on this pair
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade :param side: "long" or "short" - indicating the direction of the proposed trade
:return: A leverage amount, which is between 1.0 and max_leverage. :return: A leverage amount, which is between 1.0 and max_leverage.
""" """
return 1.0 return 1.0
@ -1036,6 +1031,8 @@ It will be called independent of the order type (entry, exit, stoploss or positi
Assuming that your strategy needs to store the high value of the candle at trade entry, this is possible with this callback as the following example show. Assuming that your strategy needs to store the high value of the candle at trade entry, this is possible with this callback as the following example show.
``` python ``` python
# Default imports
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None: def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None:
""" """
@ -1052,7 +1049,7 @@ class AwesomeStrategy(IStrategy):
last_candle = dataframe.iloc[-1].squeeze() last_candle = dataframe.iloc[-1].squeeze()
if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side): if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side):
trade.set_custom_data(key='entry_candle_high', value=last_candle['high']) trade.set_custom_data(key="entry_candle_high", value=last_candle["high"])
return None return None

View File

@ -158,7 +158,7 @@ Out of the box, freqtrade installs the following technical libraries:
- [ta-lib](https://ta-lib.github.io/ta-lib-python/) - [ta-lib](https://ta-lib.github.io/ta-lib-python/)
- [pandas-ta](https://twopirllc.github.io/pandas-ta/) - [pandas-ta](https://twopirllc.github.io/pandas-ta/)
- [technical](https://github.com/freqtrade/technical/) - [technical](https://technical.freqtrade.io)
Additional technical libraries can be installed as necessary, or custom indicators may be written / invented by the strategy author. Additional technical libraries can be installed as necessary, or custom indicators may be written / invented by the strategy author.
@ -407,6 +407,8 @@ Currently this is `pair`, which can be accessed using `metadata['pair']` - and w
The Metadata-dict should not be modified and does not persist information across multiple calls. The Metadata-dict should not be modified and does not persist information across multiple calls.
Instead, have a look at the [Storing information](strategy-advanced.md#storing-information-persistent) section. Instead, have a look at the [Storing information](strategy-advanced.md#storing-information-persistent) section.
--8<-- "includes/strategy-imports.md"
## Strategy file loading ## Strategy file loading
By default, freqtrade will attempt to load strategies from all `.py` files within `user_data/strategies`. By default, freqtrade will attempt to load strategies from all `.py` files within `user_data/strategies`.

View File

@ -13,19 +13,22 @@ Please follow the [documentation](https://www.freqtrade.io/en/stable/data-downlo
import os import os
from pathlib import Path from pathlib import Path
# Change directory # Change directory
# Modify this cell to insure that the output shows the correct path. # Modify this cell to insure that the output shows the correct path.
# Define all paths relative to the project root shown in the cell output # Define all paths relative to the project root shown in the cell output
project_root = "somedir/freqtrade" project_root = "somedir/freqtrade"
i=0 i = 0
try: try:
os.chdir(project_root) os.chdir(project_root)
assert Path('LICENSE').is_file() if not Path("LICENSE").is_file():
except: i = 0
while i<4 and (not Path('LICENSE').is_file()): while i < 4 and (not Path("LICENSE").is_file()):
os.chdir(Path(Path.cwd(), '../')) os.chdir(Path(Path.cwd(), "../"))
i+=1 i += 1
project_root = Path.cwd() project_root = Path.cwd()
except FileNotFoundError:
print("Please define the project root relative to the current directory")
print(Path.cwd()) print(Path.cwd())
``` ```
@ -35,6 +38,7 @@ print(Path.cwd())
```python ```python
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
# Customize these according to your needs. # Customize these according to your needs.
# Initialize empty configuration object # Initialize empty configuration object
@ -58,12 +62,14 @@ pair = "BTC/USDT"
from freqtrade.data.history import load_pair_history from freqtrade.data.history import load_pair_history
from freqtrade.enums import CandleType from freqtrade.enums import CandleType
candles = load_pair_history(datadir=data_location,
timeframe=config["timeframe"], candles = load_pair_history(
pair=pair, datadir=data_location,
data_format = "json", # Make sure to update this to your data timeframe=config["timeframe"],
candle_type=CandleType.SPOT, pair=pair,
) data_format="json", # Make sure to update this to your data
candle_type=CandleType.SPOT,
)
# Confirm success # Confirm success
print(f"Loaded {len(candles)} rows of data for {pair} from {data_location}") print(f"Loaded {len(candles)} rows of data for {pair} from {data_location}")
@ -76,14 +82,16 @@ candles.head()
```python ```python
# Load strategy using values set above # Load strategy using values set above
from freqtrade.resolvers import StrategyResolver
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.resolvers import StrategyResolver
strategy = StrategyResolver.load_strategy(config) strategy = StrategyResolver.load_strategy(config)
strategy.dp = DataProvider(config, None, None) strategy.dp = DataProvider(config, None, None)
strategy.ft_bot_start() strategy.ft_bot_start()
# Generate buy/sell signals using strategy # Generate buy/sell signals using strategy
df = strategy.analyze_ticker(candles, {'pair': pair}) df = strategy.analyze_ticker(candles, {"pair": pair})
df.tail() df.tail()
``` ```
@ -102,7 +110,7 @@ df.tail()
```python ```python
# Report results # Report results
print(f"Generated {df['enter_long'].sum()} entry signals") print(f"Generated {df['enter_long'].sum()} entry signals")
data = df.set_index('date', drop=False) data = df.set_index("date", drop=False)
data.tail() data.tail()
``` ```
@ -119,10 +127,13 @@ Analyze a trades dataframe (also used below for plotting)
```python ```python
from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats
# if backtest_dir points to a directory, it'll automatically load the last backtest file. # if backtest_dir points to a directory, it'll automatically load the last backtest file.
backtest_dir = config["user_data_dir"] / "backtest_results" backtest_dir = config["user_data_dir"] / "backtest_results"
# backtest_dir can also point to a specific file # backtest_dir can also point to a specific file
# backtest_dir = config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json" # backtest_dir = (
# config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json"
# )
``` ```
@ -131,24 +142,24 @@ backtest_dir = config["user_data_dir"] / "backtest_results"
# This contains all information used to generate the backtest result. # This contains all information used to generate the backtest result.
stats = load_backtest_stats(backtest_dir) stats = load_backtest_stats(backtest_dir)
strategy = 'SampleStrategy' strategy = "SampleStrategy"
# All statistics are available per strategy, so if `--strategy-list` was used during backtest, this will be reflected here as well. # All statistics are available per strategy, so if `--strategy-list` was used during backtest,
# this will be reflected here as well.
# Example usages: # Example usages:
print(stats['strategy'][strategy]['results_per_pair']) print(stats["strategy"][strategy]["results_per_pair"])
# Get pairlist used for this backtest # Get pairlist used for this backtest
print(stats['strategy'][strategy]['pairlist']) print(stats["strategy"][strategy]["pairlist"])
# Get market change (average change of all pairs from start to end of the backtest period) # Get market change (average change of all pairs from start to end of the backtest period)
print(stats['strategy'][strategy]['market_change']) print(stats["strategy"][strategy]["market_change"])
# Maximum drawdown () # Maximum drawdown ()
print(stats['strategy'][strategy]['max_drawdown']) print(stats["strategy"][strategy]["max_drawdown"])
# Maximum drawdown start and end # Maximum drawdown start and end
print(stats['strategy'][strategy]['drawdown_start']) print(stats["strategy"][strategy]["drawdown_start"])
print(stats['strategy'][strategy]['drawdown_end']) print(stats["strategy"][strategy]["drawdown_end"])
# Get strategy comparison (only relevant if multiple strategies were compared) # Get strategy comparison (only relevant if multiple strategies were compared)
print(stats['strategy_comparison']) print(stats["strategy_comparison"])
``` ```
@ -166,24 +177,25 @@ trades.groupby("pair")["exit_reason"].value_counts()
```python ```python
# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day) # Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)
import pandas as pd
import plotly.express as px
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.data.btanalysis import load_backtest_stats from freqtrade.data.btanalysis import load_backtest_stats
import plotly.express as px
import pandas as pd
# strategy = 'SampleStrategy' # strategy = 'SampleStrategy'
# config = Configuration.from_files(["user_data/config.json"]) # config = Configuration.from_files(["user_data/config.json"])
# backtest_dir = config["user_data_dir"] / "backtest_results" # backtest_dir = config["user_data_dir"] / "backtest_results"
stats = load_backtest_stats(backtest_dir) stats = load_backtest_stats(backtest_dir)
strategy_stats = stats['strategy'][strategy] strategy_stats = stats["strategy"][strategy]
df = pd.DataFrame(columns=['dates','equity'], data=strategy_stats['daily_profit']) df = pd.DataFrame(columns=["dates", "equity"], data=strategy_stats["daily_profit"])
df['equity_daily'] = df['equity'].cumsum() df["equity_daily"] = df["equity"].cumsum()
fig = px.line(df, x="dates", y="equity_daily") fig = px.line(df, x="dates", y="equity_daily")
fig.show() fig.show()
``` ```
### Load live trading results into a pandas dataframe ### Load live trading results into a pandas dataframe
@ -194,6 +206,7 @@ In case you did already some trading and want to analyze your performance
```python ```python
from freqtrade.data.btanalysis import load_trades_from_db from freqtrade.data.btanalysis import load_trades_from_db
# Fetch trades from database # Fetch trades from database
trades = load_trades_from_db("sqlite:///tradesv3.sqlite") trades = load_trades_from_db("sqlite:///tradesv3.sqlite")
@ -210,8 +223,9 @@ This can be useful to find the best `max_open_trades` parameter, when used with
```python ```python
from freqtrade.data.btanalysis import analyze_trade_parallelism from freqtrade.data.btanalysis import analyze_trade_parallelism
# Analyze the above # Analyze the above
parallel_trades = analyze_trade_parallelism(trades, '5m') parallel_trades = analyze_trade_parallelism(trades, "5m")
parallel_trades.plot() parallel_trades.plot()
``` ```
@ -222,23 +236,23 @@ Freqtrade offers interactive plotting capabilities based on plotly.
```python ```python
from freqtrade.plot.plotting import generate_candlestick_graph from freqtrade.plot.plotting import generate_candlestick_graph
# Limit graph period to keep plotly quick and reactive # Limit graph period to keep plotly quick and reactive
# Filter trades to one pair # Filter trades to one pair
trades_red = trades.loc[trades['pair'] == pair] trades_red = trades.loc[trades["pair"] == pair]
data_red = data['2019-06-01':'2019-06-10'] data_red = data["2019-06-01":"2019-06-10"]
# Generate candlestick graph # Generate candlestick graph
graph = generate_candlestick_graph(pair=pair, graph = generate_candlestick_graph(
data=data_red, pair=pair,
trades=trades_red, data=data_red,
indicators1=['sma20', 'ema50', 'ema55'], trades=trades_red,
indicators2=['rsi', 'macd', 'macdsignal', 'macdhist'] indicators1=["sma20", "ema50", "ema55"],
) indicators2=["rsi", "macd", "macdsignal", "macdhist"],
)
``` ```
@ -248,7 +262,6 @@ graph = generate_candlestick_graph(pair=pair,
# Render graph in a separate window # Render graph in a separate window
graph.show(renderer="browser") graph.show(renderer="browser")
``` ```
## Plot average profit per trade as distribution graph ## Plot average profit per trade as distribution graph
@ -257,12 +270,12 @@ graph.show(renderer="browser")
```python ```python
import plotly.figure_factory as ff import plotly.figure_factory as ff
hist_data = [trades.profit_ratio] hist_data = [trades.profit_ratio]
group_labels = ['profit_ratio'] # name of the dataset group_labels = ["profit_ratio"] # name of the dataset
fig = ff.create_distplot(hist_data, group_labels, bin_size=0.01) fig = ff.create_distplot(hist_data, group_labels, bin_size=0.01)
fig.show() fig.show()
``` ```
Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data.

View File

@ -11,3 +11,7 @@
.rst-versions .rst-other-versions { .rst-versions .rst-other-versions {
color: white; color: white;
} }
.md-version__list {
font-weight: 500 !important;
}

View File

@ -418,8 +418,9 @@ Common arguments:
``` ```
By default, only active pairs/markets are shown. Active pairs/markets are those that can currently be traded By default, only active pairs/markets are shown. Active pairs/markets are those that can currently be traded on the exchange.
on the exchange. The see the list of all pairs/markets (not only the active ones), use the `-a`/`-all` option. You can use the `-a`/`-all` option to see the list of all pairs/markets, including the inactive ones.
Pairs may be listed as untradeable if the smallest tradeable price for the market is very small, i.e. less than `1e-11` (`0.00000000001`)
Pairs/markets are sorted by its symbol string in the printed output. Pairs/markets are sorted by its symbol string in the printed output.

View File

@ -1,6 +1,6 @@
"""Freqtrade bot""" """Freqtrade bot"""
__version__ = "2024.7.1" __version__ = "2024.8"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path

View File

@ -15,6 +15,7 @@ from freqtrade.commands.data_commands import (
start_convert_trades, start_convert_trades,
start_download_data, start_download_data,
start_list_data, start_list_data,
start_list_trades_data,
) )
from freqtrade.commands.db_commands import start_convert_db from freqtrade.commands.db_commands import start_convert_db
from freqtrade.commands.deploy_commands import ( from freqtrade.commands.deploy_commands import (

View File

@ -132,7 +132,15 @@ ARGS_CONVERT_TRADES = [
"trading_mode", "trading_mode",
] ]
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode", "show_timerange"] ARGS_LIST_DATA = [
"exchange",
"dataformat_ohlcv",
"dataformat_trades",
"trades",
"pairs",
"trading_mode",
"show_timerange",
]
ARGS_DOWNLOAD_DATA = [ ARGS_DOWNLOAD_DATA = [
"pairs", "pairs",

View File

@ -446,8 +446,12 @@ AVAILABLE_CLI_OPTIONS = {
), ),
"download_trades": Arg( "download_trades": Arg(
"--dl-trades", "--dl-trades",
help="Download trades instead of OHLCV data. The bot will resample trades to the " help="Download trades instead of OHLCV data.",
"desired timeframe as specified as --timeframes/-t.", action="store_true",
),
"trades": Arg(
"--trades",
help="Work on trades data instead of OHLCV data.",
action="store_true", action="store_true",
), ),
"convert_trades": Arg( "convert_trades": Arg(

View File

@ -14,6 +14,7 @@ from freqtrade.data.history import download_data_main
from freqtrade.enums import CandleType, RunMode, TradingMode from freqtrade.enums import CandleType, RunMode, TradingMode
from freqtrade.exceptions import ConfigurationError from freqtrade.exceptions import ConfigurationError
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import plural
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.util import print_rich_table from freqtrade.util import print_rich_table
@ -115,9 +116,13 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
def start_list_data(args: Dict[str, Any]) -> None: def start_list_data(args: Dict[str, Any]) -> None:
""" """
List available backtest data List available OHLCV data
""" """
if args["trades"]:
start_list_trades_data(args)
return
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
from freqtrade.data.history import get_datahandler from freqtrade.data.history import get_datahandler
@ -127,7 +132,6 @@ def start_list_data(args: Dict[str, Any]) -> None:
paircombs = dhc.ohlcv_get_available_data( paircombs = dhc.ohlcv_get_available_data(
config["datadir"], config.get("trading_mode", TradingMode.SPOT) config["datadir"], config.get("trading_mode", TradingMode.SPOT)
) )
if args["pairs"]: if args["pairs"]:
paircombs = [comb for comb in paircombs if comb[0] in args["pairs"]] paircombs = [comb for comb in paircombs if comb[0] in args["pairs"]]
title = f"Found {len(paircombs)} pair / timeframe combinations." title = f"Found {len(paircombs)} pair / timeframe combinations."
@ -171,3 +175,51 @@ def start_list_data(args: Dict[str, Any]) -> None:
summary=title, summary=title,
table_kwargs={"min_width": 50}, table_kwargs={"min_width": 50},
) )
def start_list_trades_data(args: Dict[str, Any]) -> None:
"""
List available Trades data
"""
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
from freqtrade.data.history import get_datahandler
dhc = get_datahandler(config["datadir"], config["dataformat_trades"])
paircombs = dhc.trades_get_available_data(
config["datadir"], config.get("trading_mode", TradingMode.SPOT)
)
if args["pairs"]:
paircombs = [comb for comb in paircombs if comb in args["pairs"]]
title = f"Found trades data for {len(paircombs)} {plural(len(paircombs), 'pair')}."
if not config.get("show_timerange"):
print_rich_table(
[(pair, config.get("candle_type_def", CandleType.SPOT)) for pair in sorted(paircombs)],
("Pair", "Type"),
title,
table_kwargs={"min_width": 50},
)
else:
paircombs1 = [
(pair, *dhc.trades_data_min_max(pair, config.get("trading_mode", TradingMode.SPOT)))
for pair in paircombs
]
print_rich_table(
[
(
pair,
config.get("candle_type_def", CandleType.SPOT),
start.strftime(DATETIME_PRINT_FORMAT),
end.strftime(DATETIME_PRINT_FORMAT),
str(length),
)
for pair, start, end, length in sorted(paircombs1, key=lambda x: (x[0]))
],
("Pair", "Type", "From", "To", "Trades"),
summary=title,
table_kwargs={"min_width": 50},
)

View File

@ -32,7 +32,7 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
) )
if args["print_one_column"]: if args["print_one_column"]:
print("\n".join([e["name"] for e in available_exchanges])) print("\n".join([e["classname"] for e in available_exchanges]))
else: else:
if args["list_exchanges_all"]: if args["list_exchanges_all"]:
title = ( title = (
@ -46,14 +46,20 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
table = Table(title=title) table = Table(title=title)
table.add_column("Exchange Name") table.add_column("Exchange Name")
table.add_column("Class Name")
table.add_column("Markets") table.add_column("Markets")
table.add_column("Reason") table.add_column("Reason")
for exchange in available_exchanges: for exchange in available_exchanges:
name = Text(exchange["name"]) name = Text(exchange["name"])
if exchange["supported"]: if exchange["supported"]:
name.append(" (Official)", style="italic") name.append(" (Supported)", style="italic")
name.stylize("green bold") name.stylize("green bold")
classname = Text(exchange["classname"])
if exchange["is_alias"]:
name.stylize("strike")
classname.stylize("strike")
classname.append(f" (use {exchange['alias_for']})", style="italic")
trade_modes = Text( trade_modes = Text(
", ".join( ", ".join(
@ -68,6 +74,7 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
table.add_row( table.add_row(
name, name,
classname,
trade_modes, trade_modes,
exchange["comment"], exchange["comment"],
style=None if exchange["valid"] else "red", style=None if exchange["valid"] else "red",

View File

@ -1,5 +1,6 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.configuration.asyncio_config import asyncio_setup
from freqtrade.configuration.config_secrets import sanitize_config from freqtrade.configuration.config_secrets import sanitize_config
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

View File

@ -0,0 +1,10 @@
import sys
def asyncio_setup() -> None: # pragma: no cover
# Set eventloop for win32 setups
if sys.platform == "win32":
import asyncio
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

View File

@ -36,11 +36,6 @@ CONF_SCHEMA = {
"type": ["integer", "number"], "type": ["integer", "number"],
"minimum": -1, "minimum": -1,
}, },
"new_pairs_days": {
"description": "Download data of new pairs for given number of days",
"type": "integer",
"default": 30,
},
"timeframe": { "timeframe": {
"description": ( "description": (
f"The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). {__IN_STRATEGY}" f"The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). {__IN_STRATEGY}"
@ -185,6 +180,7 @@ CONF_SCHEMA = {
"type": "boolean", "type": "boolean",
"default": False, "default": False,
}, },
# Lookahead analysis section
"minimum_trade_amount": { "minimum_trade_amount": {
"description": "Minimum amount for a trade - only used for lookahead-analysis", "description": "Minimum amount for a trade - only used for lookahead-analysis",
"type": "number", "type": "number",
@ -480,6 +476,12 @@ CONF_SCHEMA = {
"type": "number", "type": "number",
"minimum": 0, "minimum": 0,
}, },
"unlock_at": {
"description": (
"Time when trading will be unlocked regularly. Format: HH:MM"
),
"type": "string",
},
"trade_limit": { "trade_limit": {
"description": "Minimum number of trades required during lookback period.", "description": "Minimum number of trades required during lookback period.",
"type": "number", "type": "number",
@ -501,6 +503,7 @@ CONF_SCHEMA = {
"required": ["method"], "required": ["method"],
}, },
}, },
# RPC section
"telegram": { "telegram": {
"description": "Telegram settings.", "description": "Telegram settings.",
"type": "object", "type": "object",
@ -701,6 +704,7 @@ CONF_SCHEMA = {
}, },
"required": ["enabled", "listen_ip_address", "listen_port", "username", "password"], "required": ["enabled", "listen_ip_address", "listen_port", "username", "password"],
}, },
# end of RPC section
"db_url": { "db_url": {
"description": "Database connection URL.", "description": "Database connection URL.",
"type": "string", "type": "string",
@ -734,7 +738,7 @@ CONF_SCHEMA = {
"default": {}, "default": {},
"properties": { "properties": {
"process_throttle_secs": { "process_throttle_secs": {
"description": "Throttle time in seconds for processing.", "description": "Minimum loop duration for one bot iteration in seconds.",
"type": "integer", "type": "integer",
}, },
"interval": { "interval": {
@ -763,11 +767,26 @@ CONF_SCHEMA = {
"description": f"Enable position adjustment. {__IN_STRATEGY}", "description": f"Enable position adjustment. {__IN_STRATEGY}",
"type": "boolean", "type": "boolean",
}, },
# Download data section
"new_pairs_days": {
"description": "Download data of new pairs for given number of days",
"type": "integer",
"default": 30,
},
"download_trades": {
"description": "Download trades data by default (instead of ohlcv data).",
"type": "boolean",
},
"max_entry_position_adjustment": { "max_entry_position_adjustment": {
"description": f"Maximum entry position adjustment allowed. {__IN_STRATEGY}", "description": f"Maximum entry position adjustment allowed. {__IN_STRATEGY}",
"type": ["integer", "number"], "type": ["integer", "number"],
"minimum": -1, "minimum": -1,
}, },
"add_config_files": {
"description": "Additional configuration files to load.",
"type": "array",
"items": {"type": "string"},
},
"orderflow": { "orderflow": {
"description": "Settings related to order flow.", "description": "Settings related to order flow.",
"type": "object", "type": "object",
@ -853,6 +872,14 @@ CONF_SCHEMA = {
"items": {"type": "string"}, "items": {"type": "string"},
"uniqueItems": True, "uniqueItems": True,
}, },
"log_responses": {
"description": (
"Log responses from the exchange."
"Useful/required to debug issues with order processing."
),
"type": "boolean",
"default": False,
},
"unknown_fee_rate": { "unknown_fee_rate": {
"description": "Fee rate for unknown markets.", "description": "Fee rate for unknown markets.",
"type": "number", "type": "number",

View File

@ -14,12 +14,16 @@ def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
return config return config
keys_to_remove = [ keys_to_remove = [
"exchange.key", "exchange.key",
"exchange.api_key",
"exchange.apiKey", "exchange.apiKey",
"exchange.secret", "exchange.secret",
"exchange.password", "exchange.password",
"exchange.uid", "exchange.uid",
"exchange.account_id",
"exchange.accountId", "exchange.accountId",
"exchange.wallet_address",
"exchange.walletAddress", "exchange.walletAddress",
"exchange.private_key",
"exchange.privateKey", "exchange.privateKey",
"telegram.token", "telegram.token",
"telegram.chat_id", "telegram.chat_id",
@ -33,8 +37,10 @@ def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
nested_config = config nested_config = config
for nested_key in nested_keys[:-1]: for nested_key in nested_keys[:-1]:
nested_config = nested_config.get(nested_key, {}) nested_config = nested_config.get(nested_key, {})
nested_config[nested_keys[-1]] = "REDACTED" if nested_keys[-1] in nested_config:
nested_config[nested_keys[-1]] = "REDACTED"
else: else:
config[key] = "REDACTED" if key in config:
config[key] = "REDACTED"
return config return config

View File

@ -1,6 +1,7 @@
import logging import logging
from collections import Counter from collections import Counter
from copy import deepcopy from copy import deepcopy
from datetime import datetime
from typing import Any, Dict from typing import Any, Dict
from jsonschema import Draft4Validator, validators from jsonschema import Draft4Validator, validators
@ -201,16 +202,32 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
""" """
for prot in conf.get("protections", []): for prot in conf.get("protections", []):
parsed_unlock_at = None
if (config_unlock_at := prot.get("unlock_at")) is not None:
try:
parsed_unlock_at = datetime.strptime(config_unlock_at, "%H:%M")
except ValueError:
raise ConfigurationError(f"Invalid date format for unlock_at: {config_unlock_at}.")
if "stop_duration" in prot and "stop_duration_candles" in prot: if "stop_duration" in prot and "stop_duration_candles" in prot:
raise ConfigurationError( raise ConfigurationError(
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n" "Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
f"Please fix the protection {prot.get('method')}" f"Please fix the protection {prot.get('method')}."
) )
if "lookback_period" in prot and "lookback_period_candles" in prot: if "lookback_period" in prot and "lookback_period_candles" in prot:
raise ConfigurationError( raise ConfigurationError(
"Protections must specify either `lookback_period` or `lookback_period_candles`.\n" "Protections must specify either `lookback_period` or `lookback_period_candles`.\n"
f"Please fix the protection {prot.get('method')}" f"Please fix the protection {prot.get('method')}."
)
if parsed_unlock_at is not None and (
"stop_duration" in prot or "stop_duration_candles" in prot
):
raise ConfigurationError(
"Protections must specify either `unlock_at`, `stop_duration` or "
"`stop_duration_candles`.\n"
f"Please fix the protection {prot.get('method')}."
) )

View File

@ -42,6 +42,7 @@ HYPEROPT_LOSS_BUILTIN = [
AVAILABLE_PAIRLISTS = [ AVAILABLE_PAIRLISTS = [
"StaticPairList", "StaticPairList",
"VolumePairList", "VolumePairList",
"PercentChangePairList",
"ProducerPairList", "ProducerPairList",
"RemotePairList", "RemotePairList",
"MarketCapPairList", "MarketCapPairList",

View File

@ -401,7 +401,15 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
timeframe_freq = timeframe_to_resample_freq(timeframe) timeframe_freq = timeframe_to_resample_freq(timeframe)
dates = [ dates = [
pd.Series(pd.date_range(row[1]["open_date"], row[1]["close_date"], freq=timeframe_freq)) pd.Series(
pd.date_range(
row[1]["open_date"],
row[1]["close_date"],
freq=timeframe_freq,
# Exclude right boundary - the date is the candle open date.
inclusive="left",
)
)
for row in results[["open_date", "close_date"]].iterrows() for row in results[["open_date", "close_date"]].iterrows()
] ]
deltas = [len(x) for x in dates] deltas = [len(x) for x in dates]

View File

@ -78,6 +78,8 @@ def populate_dataframe_with_trades(
# create columns for trades # create columns for trades
_init_dataframe_with_trades_columns(dataframe) _init_dataframe_with_trades_columns(dataframe)
if trades is None or trades.empty:
return dataframe, cached_grouped_trades
try: try:
start_time = time.time() start_time = time.time()
@ -88,7 +90,7 @@ def populate_dataframe_with_trades(
max_candles = config_orderflow["max_candles"] max_candles = config_orderflow["max_candles"]
start_date = dataframe.tail(max_candles).date.iat[0] start_date = dataframe.tail(max_candles).date.iat[0]
# slice of trades that are before current ohlcv candles to make groupby faster # slice of trades that are before current ohlcv candles to make groupby faster
trades = trades.loc[trades.candle_start >= start_date] trades = trades.loc[trades["candle_start"] >= start_date]
trades.reset_index(inplace=True, drop=True) trades.reset_index(inplace=True, drop=True)
# group trades by candle start # group trades by candle start

View File

@ -521,15 +521,12 @@ class DataProvider:
(pair, timeframe or self._config["timeframe"], _candle_type), copy=copy (pair, timeframe or self._config["timeframe"], _candle_type), copy=copy
) )
elif self.runmode in (RunMode.BACKTEST, RunMode.HYPEROPT): elif self.runmode in (RunMode.BACKTEST, RunMode.HYPEROPT):
_candle_type = (
CandleType.from_string(candle_type)
if candle_type != ""
else self._config["candle_type_def"]
)
data_handler = get_datahandler( data_handler = get_datahandler(
self._config["datadir"], data_format=self._config["dataformat_trades"] self._config["datadir"], data_format=self._config["dataformat_trades"]
) )
trades_df = data_handler.trades_load(pair, TradingMode.FUTURES) trades_df = data_handler.trades_load(
pair, self._config.get("trading_mode", TradingMode.SPOT)
)
return trades_df return trades_df
else: else:

View File

@ -12,7 +12,7 @@ from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import List, Optional, Tuple, Type from typing import List, Optional, Tuple, Type
from pandas import DataFrame from pandas import DataFrame, to_datetime
from freqtrade import misc from freqtrade import misc
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
@ -32,6 +32,7 @@ logger = logging.getLogger(__name__)
class IDataHandler(ABC): class IDataHandler(ABC):
_OHLCV_REGEX = r"^([a-zA-Z_\d-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)" _OHLCV_REGEX = r"^([a-zA-Z_\d-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)"
_TRADES_REGEX = r"^([a-zA-Z_\d-]+)\-(trades)?(?=\.)"
def __init__(self, datadir: Path) -> None: def __init__(self, datadir: Path) -> None:
self._datadir = datadir self._datadir = datadir
@ -166,6 +167,50 @@ class IDataHandler(ABC):
:param candle_type: Any of the enum CandleType (must match trading mode!) :param candle_type: Any of the enum CandleType (must match trading mode!)
""" """
@classmethod
def trades_get_available_data(cls, datadir: Path, trading_mode: TradingMode) -> List[str]:
"""
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, CandleType)
"""
if trading_mode == TradingMode.FUTURES:
datadir = datadir.joinpath("futures")
_tmp = [
re.search(cls._TRADES_REGEX, p.name)
for p in datadir.glob(f"*.{cls._get_file_extension()}")
]
return [
cls.rebuild_pair_from_filename(match[1])
for match in _tmp
if match and len(match.groups()) > 1
]
def trades_data_min_max(
self,
pair: str,
trading_mode: TradingMode,
) -> Tuple[datetime, datetime, int]:
"""
Returns the min and max timestamp for the given pair's trades data.
:param pair: Pair to get min/max for
:param trading_mode: Trading mode to use (used to determine the filename)
:return: (min, max, len)
"""
df = self._trades_load(pair, trading_mode)
if df.empty:
return (
datetime.fromtimestamp(0, tz=timezone.utc),
datetime.fromtimestamp(0, tz=timezone.utc),
0,
)
return (
to_datetime(df.iloc[0]["timestamp"], unit="ms", utc=True).to_pydatetime(),
to_datetime(df.iloc[-1]["timestamp"], unit="ms", utc=True).to_pydatetime(),
len(df),
)
@classmethod @classmethod
def trades_get_pairs(cls, datadir: Path) -> List[str]: def trades_get_pairs(cls, datadir: Path) -> List[str]:
""" """
@ -247,9 +292,13 @@ class IDataHandler(ABC):
:param timerange: Timerange to load trades for - currently not implemented :param timerange: Timerange to load trades for - currently not implemented
:return: List of trades :return: List of trades
""" """
trades = trades_df_remove_duplicates( try:
self._trades_load(pair, trading_mode, timerange=timerange) trades = self._trades_load(pair, trading_mode, timerange=timerange)
) except Exception:
logger.exception(f"Error loading trades for {pair}")
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
trades = trades_df_remove_duplicates(trades)
trades = trades_convert_types(trades) trades = trades_convert_types(trades)
return trades return trades

View File

@ -39,6 +39,7 @@ from freqtrade.exchange.exchange_utils_timeframe import (
from freqtrade.exchange.gate import Gate from freqtrade.exchange.gate import Gate
from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.hitbtc import Hitbtc
from freqtrade.exchange.htx import Htx from freqtrade.exchange.htx import Htx
from freqtrade.exchange.hyperliquid import Hyperliquid
from freqtrade.exchange.idex import Idex from freqtrade.exchange.idex import Idex
from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kraken import Kraken
from freqtrade.exchange.kucoin import Kucoin from freqtrade.exchange.kucoin import Kucoin

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,10 @@
"""Kucoin exchange subclass.""" """Bitvavo exchange subclass."""
import logging import logging
from typing import Dict from typing import Dict
from ccxt import DECIMAL_PLACES
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -22,3 +24,11 @@ class Bitvavo(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"ohlcv_candle_limit": 1440, "ohlcv_candle_limit": 1440,
} }
@property
def precisionMode(self) -> int:
"""
Exchange ccxt precisionMode
Override due to https://github.com/ccxt/ccxt/issues/20408
"""
return DECIMAL_PLACES

View File

@ -88,7 +88,14 @@ from freqtrade.exchange.exchange_utils_timeframe import (
timeframe_to_seconds, timeframe_to_seconds,
) )
from freqtrade.exchange.exchange_ws import ExchangeWS from freqtrade.exchange.exchange_ws import ExchangeWS
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers from freqtrade.exchange.types import (
CcxtBalances,
CcxtPosition,
OHLCVResponse,
OrderBook,
Ticker,
Tickers,
)
from freqtrade.misc import ( from freqtrade.misc import (
chunks, chunks,
deep_merge_dicts, deep_merge_dicts,
@ -128,6 +135,7 @@ class Exchange:
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
"ohlcv_volume_currency": "base", # "base" or "quote" "ohlcv_volume_currency": "base", # "base" or "quote"
"tickers_have_quoteVolume": True, "tickers_have_quoteVolume": True,
"tickers_have_percentage": True,
"tickers_have_bid_ask": True, # bid / ask empty for fetch_tickers "tickers_have_bid_ask": True, # bid / ask empty for fetch_tickers
"tickers_have_price": True, "tickers_have_price": True,
"trades_limit": 1000, # Limit for 1 call to fetch_trades "trades_limit": 1000, # Limit for 1 call to fetch_trades
@ -315,7 +323,7 @@ class Exchange:
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
return loop return loop
def validate_config(self, config): def validate_config(self, config: Config) -> None:
# Check if timeframe is available # Check if timeframe is available
self.validate_timeframes(config.get("timeframe")) self.validate_timeframes(config.get("timeframe"))
@ -329,6 +337,7 @@ class Exchange:
self.validate_pricing(config["exit_pricing"]) self.validate_pricing(config["exit_pricing"])
self.validate_pricing(config["entry_pricing"]) self.validate_pricing(config["entry_pricing"])
self.validate_orderflow(config["exchange"]) self.validate_orderflow(config["exchange"])
self.validate_freqai(config)
def _init_ccxt( def _init_ccxt(
self, exchange_config: Dict[str, Any], sync: bool, ccxt_kwargs: Dict[str, Any] self, exchange_config: Dict[str, Any], sync: bool, ccxt_kwargs: Dict[str, Any]
@ -352,14 +361,18 @@ class Exchange:
raise OperationalException(f"Exchange {name} is not supported by ccxt") raise OperationalException(f"Exchange {name} is not supported by ccxt")
ex_config = { ex_config = {
"apiKey": exchange_config.get("apiKey", exchange_config.get("key")), "apiKey": exchange_config.get(
"api_key", exchange_config.get("apiKey", exchange_config.get("key"))
),
"secret": exchange_config.get("secret"), "secret": exchange_config.get("secret"),
"password": exchange_config.get("password"), "password": exchange_config.get("password"),
"uid": exchange_config.get("uid", ""), "uid": exchange_config.get("uid", ""),
"accountId": exchange_config.get("accountId", ""), "accountId": exchange_config.get("account_id", exchange_config.get("accountId", "")),
# DEX attributes: # DEX attributes:
"walletAddress": exchange_config.get("walletAddress"), "walletAddress": exchange_config.get(
"privateKey": exchange_config.get("privateKey"), "wallet_address", exchange_config.get("walletAddress")
),
"privateKey": exchange_config.get("private_key", exchange_config.get("privateKey")),
} }
if ccxt_kwargs: if ccxt_kwargs:
logger.info("Applying additional ccxt config: %s", ccxt_kwargs) logger.info("Applying additional ccxt config: %s", ccxt_kwargs)
@ -411,7 +424,17 @@ class Exchange:
@property @property
def precisionMode(self) -> int: def precisionMode(self) -> int:
"""exchange ccxt precisionMode""" """Exchange ccxt precisionMode"""
return self._api.precisionMode
@property
def precision_mode_price(self) -> int:
"""
Exchange ccxt precisionMode used for price
Workaround for ccxt limitation to not have precisionMode for price
if it differs for an exchange
Might need to be updated if https://github.com/ccxt/ccxt/issues/20408 is fixed.
"""
return self._api.precisionMode return self._api.precisionMode
def additional_exchange_init(self) -> None: def additional_exchange_init(self) -> None:
@ -541,7 +564,7 @@ class Exchange:
else: else:
return self._trades[pair_interval] return self._trades[pair_interval]
else: else:
return DataFrame() return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
def get_contract_size(self, pair: str) -> Optional[float]: def get_contract_size(self, pair: str) -> Optional[float]:
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
@ -804,6 +827,13 @@ class Exchange:
f"Trade data not available for {self.name}. Can't use orderflow feature." f"Trade data not available for {self.name}. Can't use orderflow feature."
) )
def validate_freqai(self, config: Config) -> None:
freqai_enabled = config.get("freqai", {}).get("enabled", False)
if freqai_enabled and not self._ft_has["ohlcv_has_history"]:
raise ConfigurationError(
f"Historic OHLCV data not available for {self.name}. Can't use freqAI."
)
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int: def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
""" """
Checks if required startup_candles is more than ohlcv_candle_limit(). Checks if required startup_candles is more than ohlcv_candle_limit().
@ -908,7 +938,10 @@ class Exchange:
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts. For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
""" """
return price_to_precision( return price_to_precision(
price, self.get_precision_price(pair), self.precisionMode, rounding_mode=rounding_mode price,
self.get_precision_price(pair),
self.precision_mode_price,
rounding_mode=rounding_mode,
) )
def price_get_one_pip(self, pair: str, price: float) -> float: def price_get_one_pip(self, pair: str, price: float) -> float:
@ -1645,7 +1678,7 @@ class Exchange:
return order return order
@retrier @retrier
def get_balances(self) -> dict: def get_balances(self) -> CcxtBalances:
try: try:
balances = self._api.fetch_balance() balances = self._api.fetch_balance()
# Remove additional info from ccxt results # Remove additional info from ccxt results
@ -1665,7 +1698,7 @@ class Exchange:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier @retrier
def fetch_positions(self, pair: Optional[str] = None) -> List[Dict]: def fetch_positions(self, pair: Optional[str] = None) -> List[CcxtPosition]:
""" """
Fetch positions from the exchange. Fetch positions from the exchange.
If no pair is given, all positions are returned. If no pair is given, all positions are returned.
@ -1677,7 +1710,7 @@ class Exchange:
symbols = [] symbols = []
if pair: if pair:
symbols.append(pair) symbols.append(pair)
positions: List[Dict] = self._api.fetch_positions(symbols) positions: List[CcxtPosition] = 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:
@ -2469,17 +2502,17 @@ class Exchange:
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list)) logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
# Gather coroutines to run # Gather coroutines to run
input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache) ohlcv_dl_jobs, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
results_df = {} results_df = {}
# Chunk requests into batches of 100 to avoid overwhelming ccxt Throttling # Chunk requests into batches of 100 to avoid overwhelming ccxt Throttling
for input_coro in chunks(input_coroutines, 100): for dl_jobs_batch in chunks(ohlcv_dl_jobs, 100):
async def gather_stuff(coro): async def gather_coroutines(coro):
return await asyncio.gather(*coro, return_exceptions=True) return await asyncio.gather(*coro, return_exceptions=True)
with self._loop_lock: with self._loop_lock:
results = self.loop.run_until_complete(gather_stuff(input_coro)) results = self.loop.run_until_complete(gather_coroutines(dl_jobs_batch))
for res in results: for res in results:
if isinstance(res, Exception): if isinstance(res, Exception):
@ -2607,12 +2640,13 @@ class Exchange:
except (ccxt.OperationFailed, ccxt.ExchangeError) as e: except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f"Could not fetch historical candle (OHLCV) data " f"Could not fetch historical candle (OHLCV) data "
f"for pair {pair} due to {e.__class__.__name__}. " f"for {pair}, {timeframe}, {candle_type} due to {e.__class__.__name__}. "
f"Message: {e}" f"Message: {e}"
) from e ) from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException( raise OperationalException(
f"Could not fetch historical candle (OHLCV) data for pair {pair}. Message: {e}" f"Could not fetch historical candle (OHLCV) data for "
f"{pair}, {timeframe}, {candle_type}. Message: {e}"
) from e ) from e
async def _fetch_funding_rate_history( async def _fetch_funding_rate_history(
@ -2677,6 +2711,94 @@ class Exchange:
self._trades[(pair, timeframe, c_type)] = trades_df self._trades[(pair, timeframe, c_type)] = trades_df
return trades_df return trades_df
async def _build_trades_dl_jobs(
self, pairwt: PairWithTimeframe, data_handler, cache: bool
) -> Tuple[PairWithTimeframe, Optional[DataFrame]]:
"""
Build coroutines to refresh trades for (they're then called through async.gather)
"""
pair, timeframe, candle_type = pairwt
since_ms = None
new_ticks: List = []
all_stored_ticks_df = DataFrame(columns=DEFAULT_TRADES_COLUMNS + ["date"])
first_candle_ms = self.needed_candle_for_trades_ms(timeframe, candle_type)
# refresh, if
# a. not in _trades
# b. no cache used
# c. need new data
is_in_cache = (pair, timeframe, candle_type) in self._trades
if (
not is_in_cache
or not cache
or self._now_is_time_to_refresh_trades(pair, timeframe, candle_type)
):
logger.debug(f"Refreshing TRADES data for {pair}")
# fetch trades since latest _trades and
# store together with existing trades
try:
until = None
from_id = None
if is_in_cache:
from_id = self._trades[(pair, timeframe, candle_type)].iloc[-1]["id"]
until = dt_ts() # now
else:
until = int(timeframe_to_prev_date(timeframe).timestamp()) * 1000
all_stored_ticks_df = data_handler.trades_load(
f"{pair}-cached", self.trading_mode
)
if not all_stored_ticks_df.empty:
if (
all_stored_ticks_df.iloc[-1]["timestamp"] > first_candle_ms
and all_stored_ticks_df.iloc[0]["timestamp"] <= first_candle_ms
):
# Use cache and populate further
last_cached_ms = all_stored_ticks_df.iloc[-1]["timestamp"]
from_id = all_stored_ticks_df.iloc[-1]["id"]
# only use cached if it's closer than first_candle_ms
since_ms = (
last_cached_ms
if last_cached_ms > first_candle_ms
else first_candle_ms
)
else:
# Skip cache, it's too old
all_stored_ticks_df = DataFrame(
columns=DEFAULT_TRADES_COLUMNS + ["date"]
)
# from_id overrules with exchange set to id paginate
[_, new_ticks] = await self._async_get_trade_history(
pair,
since=since_ms if since_ms else first_candle_ms,
until=until,
from_id=from_id,
)
except Exception:
logger.exception(f"Refreshing TRADES data for {pair} failed")
return pairwt, None
if new_ticks:
all_stored_ticks_list = all_stored_ticks_df[DEFAULT_TRADES_COLUMNS].values.tolist()
all_stored_ticks_list.extend(new_ticks)
trades_df = self._process_trades_df(
pair,
timeframe,
candle_type,
all_stored_ticks_list,
cache,
first_required_candle_date=first_candle_ms,
)
data_handler.trades_store(
f"{pair}-cached", trades_df[DEFAULT_TRADES_COLUMNS], self.trading_mode
)
return pairwt, trades_df
else:
logger.error(f"No new ticks for {pair}")
return pairwt, None
def refresh_latest_trades( def refresh_latest_trades(
self, self,
pair_list: ListPairsWithTimeframes, pair_list: ListPairsWithTimeframes,
@ -2697,90 +2819,25 @@ class Exchange:
self._config["datadir"], data_format=self._config["dataformat_trades"] self._config["datadir"], data_format=self._config["dataformat_trades"]
) )
logger.debug("Refreshing TRADES data for %d pairs", len(pair_list)) logger.debug("Refreshing TRADES data for %d pairs", len(pair_list))
since_ms = None
results_df = {} results_df = {}
for pair, timeframe, candle_type in set(pair_list): trades_dl_jobs = []
new_ticks: List = [] for pair_wt in set(pair_list):
all_stored_ticks_df = DataFrame(columns=DEFAULT_TRADES_COLUMNS + ["date"]) trades_dl_jobs.append(self._build_trades_dl_jobs(pair_wt, data_handler, cache))
first_candle_ms = self.needed_candle_for_trades_ms(timeframe, candle_type)
# refresh, if
# a. not in _trades
# b. no cache used
# c. need new data
is_in_cache = (pair, timeframe, candle_type) in self._trades
if (
not is_in_cache
or not cache
or self._now_is_time_to_refresh_trades(pair, timeframe, candle_type)
):
logger.debug(f"Refreshing TRADES data for {pair}")
# fetch trades since latest _trades and
# store together with existing trades
try:
until = None
from_id = None
if is_in_cache:
from_id = self._trades[(pair, timeframe, candle_type)].iloc[-1]["id"]
until = dt_ts() # now
else: async def gather_coroutines(coro):
until = int(timeframe_to_prev_date(timeframe).timestamp()) * 1000 return await asyncio.gather(*coro, return_exceptions=True)
all_stored_ticks_df = data_handler.trades_load(
f"{pair}-cached", self.trading_mode
)
if not all_stored_ticks_df.empty: for dl_job_chunk in chunks(trades_dl_jobs, 100):
if ( with self._loop_lock:
all_stored_ticks_df.iloc[-1]["timestamp"] > first_candle_ms results = self.loop.run_until_complete(gather_coroutines(dl_job_chunk))
and all_stored_ticks_df.iloc[0]["timestamp"] <= first_candle_ms
):
# Use cache and populate further
last_cached_ms = all_stored_ticks_df.iloc[-1]["timestamp"]
from_id = all_stored_ticks_df.iloc[-1]["id"]
# only use cached if it's closer than first_candle_ms
since_ms = (
last_cached_ms
if last_cached_ms > first_candle_ms
else first_candle_ms
)
else:
# Skip cache, it's too old
all_stored_ticks_df = DataFrame(
columns=DEFAULT_TRADES_COLUMNS + ["date"]
)
# from_id overrules with exchange set to id paginate for res in results:
[_, new_ticks] = self.get_historic_trades( if isinstance(res, Exception):
pair, logger.warning(f"Async code raised an exception: {repr(res)}")
since=since_ms if since_ms else first_candle_ms,
until=until,
from_id=from_id,
)
except Exception:
logger.exception(f"Refreshing TRADES data for {pair} failed")
continue continue
pairwt, trades_df = res
if new_ticks: if trades_df is not None:
all_stored_ticks_list = all_stored_ticks_df[ results_df[pairwt] = trades_df
DEFAULT_TRADES_COLUMNS
].values.tolist()
all_stored_ticks_list.extend(new_ticks)
trades_df = self._process_trades_df(
pair,
timeframe,
candle_type,
all_stored_ticks_list,
cache,
first_required_candle_date=first_candle_ms,
)
results_df[(pair, timeframe, candle_type)] = trades_df
data_handler.trades_store(
f"{pair}-cached", trades_df[DEFAULT_TRADES_COLUMNS], self.trading_mode
)
else:
logger.error(f"No new ticks for {pair}")
return results_df return results_df

View File

@ -2,6 +2,7 @@
Exchange support utils Exchange support utils
""" """
import inspect
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import ceil, floor from math import ceil, floor
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
@ -53,9 +54,9 @@ def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[st
return [x for x in exchanges if validate_exchange(x)[0]] return [x for x in exchanges if validate_exchange(x)[0]]
def validate_exchange(exchange: str) -> Tuple[bool, str, bool]: def validate_exchange(exchange: str) -> Tuple[bool, str, Optional[ccxt.Exchange]]:
""" """
returns: can_use, reason returns: can_use, reason, exchange_object
with Reason including both missing and missing_opt with Reason including both missing and missing_opt
""" """
try: try:
@ -64,11 +65,10 @@ def validate_exchange(exchange: str) -> Tuple[bool, str, bool]:
ex_mod = getattr(ccxt.async_support, exchange.lower())() ex_mod = getattr(ccxt.async_support, exchange.lower())()
if not ex_mod or not ex_mod.has: if not ex_mod or not ex_mod.has:
return False, "", False return False, "", None
result = True result = True
reason = "" reason = ""
is_dex = getattr(ex_mod, "dex", False)
missing = [ missing = [
k k
for k, v in EXCHANGE_HAS_REQUIRED.items() for k, v in EXCHANGE_HAS_REQUIRED.items()
@ -87,19 +87,24 @@ def validate_exchange(exchange: str) -> Tuple[bool, str, bool]:
if missing_opt: if missing_opt:
reason += f"{'. ' if reason else ''}missing opt: {', '.join(missing_opt)}. " reason += f"{'. ' if reason else ''}missing opt: {', '.join(missing_opt)}. "
return result, reason, is_dex return result, reason, ex_mod
def _build_exchange_list_entry( def _build_exchange_list_entry(
exchange_name: str, exchangeClasses: Dict[str, Any] exchange_name: str, exchangeClasses: Dict[str, Any]
) -> ValidExchangesType: ) -> ValidExchangesType:
valid, comment, is_dex = validate_exchange(exchange_name) valid, comment, ex_mod = validate_exchange(exchange_name)
result: ValidExchangesType = { result: ValidExchangesType = {
"name": exchange_name, "name": getattr(ex_mod, "name", exchange_name),
"classname": exchange_name,
"valid": valid, "valid": valid,
"supported": exchange_name.lower() in SUPPORTED_EXCHANGES, "supported": exchange_name.lower() in SUPPORTED_EXCHANGES,
"comment": comment, "comment": comment,
"dex": is_dex, "dex": getattr(ex_mod, "dex", False),
"is_alias": getattr(ex_mod, "alias", False),
"alias_for": inspect.getmro(ex_mod.__class__)[1]().id
if getattr(ex_mod, "alias", False)
else None,
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}], "trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
} }
if resolved := exchangeClasses.get(exchange_name.lower()): if resolved := exchangeClasses.get(exchange_name.lower()):

View File

@ -3,6 +3,8 @@
import logging import logging
from typing import Dict from typing import Dict
from ccxt import SIGNIFICANT_DIGITS
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -17,8 +19,15 @@ class Hyperliquid(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
# Only the most recent 5000 candles are available according to the # Only the most recent 5000 candles are available according to the
# exchange's API documentation. # exchange's API documentation.
"ohlcv_has_history": True, "ohlcv_has_history": False,
"ohlcv_candle_limit": 5000, "ohlcv_candle_limit": 5000,
"trades_has_history": False, # Trades endpoint doesn't seem available. "trades_has_history": False, # Trades endpoint doesn't seem available.
"exchange_has_overrides": {"fetchTrades": False}, "exchange_has_overrides": {"fetchTrades": False},
} }
@property
def precision_mode_price(self) -> int:
"""
Override the default precision mode for price.
"""
return SIGNIFICANT_DIGITS

View File

@ -12,7 +12,7 @@ from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import CcxtBalances, Tickers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -57,7 +57,7 @@ class Kraken(Exchange):
return super().get_tickers(symbols=symbols, cached=cached) return super().get_tickers(symbols=symbols, cached=cached)
@retrier @retrier
def get_balances(self) -> dict: def get_balances(self) -> CcxtBalances:
if self._config["dry_run"]: if self._config["dry_run"]:
return {} return {}

View File

@ -34,6 +34,7 @@ class Okx(Exchange):
"stoploss_order_types": {"limit": "limit"}, "stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"trades_has_history": False, # Endpoint doesn't have a "since" parameter "trades_has_history": False, # Endpoint doesn't have a "since" parameter
"ws.enabled": True,
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"tickers_have_quoteVolume": False, "tickers_have_quoteVolume": False,
@ -43,6 +44,7 @@ class Okx(Exchange):
PriceType.MARK: "index", PriceType.MARK: "index",
PriceType.INDEX: "mark", PriceType.INDEX: "mark",
}, },
"ws.enabled": True,
} }
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [

View File

@ -12,9 +12,13 @@ class Ticker(TypedDict):
last: Optional[float] last: Optional[float]
quoteVolume: Optional[float] quoteVolume: Optional[float]
baseVolume: Optional[float] baseVolume: Optional[float]
percentage: Optional[float]
# Several more - only listing required. # Several more - only listing required.
Tickers = Dict[str, Ticker]
class OrderBook(TypedDict): class OrderBook(TypedDict):
symbol: str symbol: str
bids: List[Tuple[float, float]] bids: List[Tuple[float, float]]
@ -24,7 +28,24 @@ class OrderBook(TypedDict):
nonce: Optional[int] nonce: Optional[int]
Tickers = Dict[str, Ticker] class CcxtBalance(TypedDict):
free: float
used: float
total: float
CcxtBalances = Dict[str, CcxtBalance]
class CcxtPosition(TypedDict):
symbol: str
side: str
contracts: float
leverage: float
collateral: Optional[float]
initialMargin: Optional[float]
liquidationPrice: Optional[float]
# pair, timeframe, candleType, OHLCV, drop last?, # pair, timeframe, candleType, OHLCV, drop last?,
OHLCVResponse = Tuple[str, str, CandleType, List, bool] OHLCVResponse = Tuple[str, str, CandleType, List, bool]

View File

@ -374,6 +374,7 @@ class FreqtradeBot(LoggingMixin):
if trade.exchange != self.exchange.id: if trade.exchange != self.exchange.id:
continue continue
trade.precision_mode = self.exchange.precisionMode trade.precision_mode = self.exchange.precisionMode
trade.precision_mode_price = self.exchange.precision_mode_price
trade.amount_precision = self.exchange.get_precision_amount(trade.pair) trade.amount_precision = self.exchange.get_precision_amount(trade.pair)
trade.price_precision = self.exchange.get_precision_price(trade.pair) trade.price_precision = self.exchange.get_precision_price(trade.pair)
trade.contract_size = self.exchange.get_contract_size(trade.pair) trade.contract_size = self.exchange.get_contract_size(trade.pair)
@ -541,7 +542,11 @@ class FreqtradeBot(LoggingMixin):
) )
else: else:
trade.exit_reason = prev_exit_reason trade.exit_reason = prev_exit_reason
total = self.wallets.get_total(trade.base_currency) if trade.base_currency else 0 total = (
self.wallets.get_owned(trade.pair, trade.base_currency)
if trade.base_currency
else 0
)
if total < trade.amount: if total < trade.amount:
if trade.fully_canceled_entry_order_count == len(trade.orders): if trade.fully_canceled_entry_order_count == len(trade.orders):
logger.warning( logger.warning(
@ -992,6 +997,7 @@ class FreqtradeBot(LoggingMixin):
amount_precision=self.exchange.get_precision_amount(pair), amount_precision=self.exchange.get_precision_amount(pair),
price_precision=self.exchange.get_precision_price(pair), price_precision=self.exchange.get_precision_price(pair),
precision_mode=self.exchange.precisionMode, precision_mode=self.exchange.precisionMode,
precision_mode_price=self.exchange.precision_mode_price,
contract_size=self.exchange.get_contract_size(pair), contract_size=self.exchange.get_contract_size(pair),
) )
stoploss = self.strategy.stoploss if not self.edge else self.edge.get_stoploss(pair) stoploss = self.strategy.stoploss if not self.edge else self.edge.get_stoploss(pair)

View File

@ -15,6 +15,7 @@ if sys.version_info < (3, 9): # pragma: no cover
from freqtrade import __version__ from freqtrade import __version__
from freqtrade.commands import Arguments from freqtrade.commands import Arguments
from freqtrade.configuration import asyncio_setup
from freqtrade.constants import DOCS_LINK from freqtrade.constants import DOCS_LINK
from freqtrade.exceptions import ConfigurationError, FreqtradeException, OperationalException from freqtrade.exceptions import ConfigurationError, FreqtradeException, OperationalException
from freqtrade.loggers import setup_logging_pre from freqtrade.loggers import setup_logging_pre
@ -33,6 +34,7 @@ def main(sysargv: Optional[List[str]] = None) -> None:
return_code: Any = 1 return_code: Any = 1
try: try:
setup_logging_pre() setup_logging_pre()
asyncio_setup()
arguments = Arguments(sysargv) arguments = Arguments(sysargv)
args = arguments.get_parsed_arg() args = arguments.get_parsed_arg()

View File

@ -128,7 +128,10 @@ def round_dict(d, n):
return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()} return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()}
def safe_value_fallback(obj: dict, key1: str, key2: Optional[str] = None, default_value=None): DictMap = Union[Dict[str, Any], Mapping[str, Any]]
def safe_value_fallback(obj: DictMap, key1: str, key2: Optional[str] = None, default_value=None):
""" """
Search a value in obj, return this if it's not None. Search a value in obj, return this if it's not None.
Then search key2 in obj - return that if it's not none - then use default_value. Then search key2 in obj - return that if it's not none - then use default_value.
@ -142,10 +145,7 @@ def safe_value_fallback(obj: dict, key1: str, key2: Optional[str] = None, defaul
return default_value return default_value
dictMap = Union[Dict[str, Any], Mapping[str, Any]] def safe_value_fallback2(dict1: DictMap, dict2: DictMap, key1: str, key2: str, default_value=None):
def safe_value_fallback2(dict1: dictMap, dict2: dictMap, key1: str, key2: str, default_value=None):
""" """
Search a value in dict1, return this if it's not None. Search a value in dict1, return this if it's not None.
Fall back to dict2 - return key2 from dict2 if it's not None. Fall back to dict2 - return key2 from dict2 if it's not None.

View File

@ -14,6 +14,7 @@ from freqtrade.loggers.set_log_levels import (
) )
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.optimize.base_analysis import BaseAnalysis, VarHolder from freqtrade.optimize.base_analysis import BaseAnalysis, VarHolder
from freqtrade.resolvers import StrategyResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,10 +22,19 @@ logger = logging.getLogger(__name__)
class RecursiveAnalysis(BaseAnalysis): class RecursiveAnalysis(BaseAnalysis):
def __init__(self, config: Dict[str, Any], strategy_obj: Dict): def __init__(self, config: Dict[str, Any], strategy_obj: Dict):
self._startup_candle = config.get("startup_candle", [199, 399, 499, 999, 1999]) self._startup_candle = list(
map(int, config.get("startup_candle", [199, 399, 499, 999, 1999]))
)
super().__init__(config, strategy_obj) super().__init__(config, strategy_obj)
strat = StrategyResolver.load_strategy(config)
self._strat_scc = strat.startup_candle_count
if self._strat_scc not in self._startup_candle:
self._startup_candle.append(self._strat_scc)
self._startup_candle.sort()
self.partial_varHolder_array: List[VarHolder] = [] self.partial_varHolder_array: List[VarHolder] = []
self.partial_varHolder_lookahead_array: List[VarHolder] = [] self.partial_varHolder_lookahead_array: List[VarHolder] = []
@ -58,9 +68,13 @@ class RecursiveAnalysis(BaseAnalysis):
values_diff = compare_df.loc[indicator] values_diff = compare_df.loc[indicator]
values_diff_self = values_diff.loc["self"] values_diff_self = values_diff.loc["self"]
values_diff_other = values_diff.loc["other"] values_diff_other = values_diff.loc["other"]
diff = (values_diff_other - values_diff_self) / values_diff_self * 100
self.dict_recursive[indicator][part.startup_candle] = f"{diff:.3f}%" if values_diff_self and values_diff_other:
diff = (values_diff_other - values_diff_self) / values_diff_self * 100
str_diff = f"{diff:.3f}%"
else:
str_diff = "NaN"
self.dict_recursive[indicator][part.startup_candle] = str_diff
else: else:
logger.info("No variance on indicator(s) found due to recursive formula.") logger.info("No variance on indicator(s) found due to recursive formula.")
@ -174,7 +188,7 @@ class RecursiveAnalysis(BaseAnalysis):
start_date_partial = end_date_full - timedelta(minutes=int(timeframe_minutes)) start_date_partial = end_date_full - timedelta(minutes=int(timeframe_minutes))
for startup_candle in self._startup_candle: for startup_candle in self._startup_candle:
self.fill_partial_varholder(start_date_partial, int(startup_candle)) self.fill_partial_varholder(start_date_partial, startup_candle)
# Restore verbosity, so it's not too quiet for the next strategy # Restore verbosity, so it's not too quiet for the next strategy
restore_verbosity_for_bias_tester() restore_verbosity_for_bias_tester()

View File

@ -17,9 +17,13 @@ class RecursiveAnalysisSubFunctions:
@staticmethod @staticmethod
def text_table_recursive_analysis_instances(recursive_instances: List[RecursiveAnalysis]): def text_table_recursive_analysis_instances(recursive_instances: List[RecursiveAnalysis]):
startups = recursive_instances[0]._startup_candle startups = recursive_instances[0]._startup_candle
strat_scc = recursive_instances[0]._strat_scc
headers = ["Indicators"] headers = ["Indicators"]
for candle in startups: for candle in startups:
headers.append(str(candle)) if candle == strat_scc:
headers.append(f"{candle} (from strategy)")
else:
headers.append(str(candle))
data = [] data = []
for inst in recursive_instances: for inst in recursive_instances:

View File

@ -181,6 +181,7 @@ class Backtesting:
self.fee = max(fee for fee in fees if fee is not None) self.fee = max(fee for fee in fees if fee is not None)
logger.info(f"Using fee {self.fee:.4%} - worst case fee from exchange (lowest tier).") logger.info(f"Using fee {self.fee:.4%} - worst case fee from exchange (lowest tier).")
self.precision_mode = self.exchange.precisionMode self.precision_mode = self.exchange.precisionMode
self.precision_mode_price = self.exchange.precision_mode_price
if self.config.get("freqai_backtest_live_models", False): if self.config.get("freqai_backtest_live_models", False):
from freqtrade.freqai.utils import get_timerange_backtest_live_models from freqtrade.freqai.utils import get_timerange_backtest_live_models
@ -329,15 +330,15 @@ class Backtesting:
else: else:
self.detail_data = {} self.detail_data = {}
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
self.funding_fee_timeframe: str = self.exchange.get_option("funding_fee_timeframe") funding_fee_timeframe: str = self.exchange.get_option("funding_fee_timeframe")
self.funding_fee_timeframe_secs: int = timeframe_to_seconds(self.funding_fee_timeframe) self.funding_fee_timeframe_secs: int = timeframe_to_seconds(funding_fee_timeframe)
mark_timeframe: str = self.exchange.get_option("mark_ohlcv_timeframe") mark_timeframe: str = self.exchange.get_option("mark_ohlcv_timeframe")
# Load additional futures data. # Load additional futures data.
funding_rates_dict = history.load_data( funding_rates_dict = history.load_data(
datadir=self.config["datadir"], datadir=self.config["datadir"],
pairs=self.pairlists.whitelist, pairs=self.pairlists.whitelist,
timeframe=self.funding_fee_timeframe, timeframe=funding_fee_timeframe,
timerange=self.timerange, timerange=self.timerange,
startup_candles=0, startup_candles=0,
fail_without_data=True, fail_without_data=True,
@ -785,7 +786,7 @@ class Backtesting:
) )
if rate is not None and rate != close_rate: if rate is not None and rate != close_rate:
close_rate = price_to_precision( close_rate = price_to_precision(
rate, trade.price_precision, self.precision_mode rate, trade.price_precision, self.precision_mode_price
) )
# We can't place orders lower than current low. # We can't place orders lower than current low.
# freqtrade does not support this in live, and the order would fill immediately # freqtrade does not support this in live, and the order would fill immediately
@ -929,7 +930,9 @@ class Backtesting:
# We can't place orders higher than current high (otherwise it'd be a stop limit entry) # We can't place orders higher than current high (otherwise it'd be a stop limit entry)
# which freqtrade does not support in live. # which freqtrade does not support in live.
if new_rate is not None and new_rate != propose_rate: if new_rate is not None and new_rate != propose_rate:
propose_rate = price_to_precision(new_rate, price_precision, self.precision_mode) propose_rate = price_to_precision(
new_rate, price_precision, self.precision_mode_price
)
if direction == "short": if direction == "short":
propose_rate = max(propose_rate, row[LOW_IDX]) propose_rate = max(propose_rate, row[LOW_IDX])
else: else:
@ -1109,6 +1112,7 @@ class Backtesting:
amount_precision=precision_amount, amount_precision=precision_amount,
price_precision=precision_price, price_precision=precision_price,
precision_mode=self.precision_mode, precision_mode=self.precision_mode,
precision_mode_price=self.precision_mode_price,
contract_size=contract_size, contract_size=contract_size,
orders=[], orders=[],
) )
@ -1332,10 +1336,9 @@ class Backtesting:
pair: str, pair: str,
current_time: datetime, current_time: datetime,
end_date: datetime, end_date: datetime,
open_trade_count_start: int,
trade_dir: Optional[LongShort], trade_dir: Optional[LongShort],
is_first: bool = True, is_first: bool = True,
) -> int: ) -> None:
""" """
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized. NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
@ -1345,7 +1348,6 @@ class Backtesting:
# 1. Manage currently open orders of active trades # 1. Manage currently open orders of active trades
if self.manage_open_orders(t, current_time, row): if self.manage_open_orders(t, current_time, row):
# Close trade # Close trade
open_trade_count_start -= 1
LocalTrade.remove_bt_trade(t) LocalTrade.remove_bt_trade(t)
self.wallets.update() self.wallets.update()
@ -1361,13 +1363,9 @@ class Backtesting:
and trade_dir is not None and trade_dir is not None
and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir) and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir)
): ):
if self.trade_slot_available(open_trade_count_start): if self.trade_slot_available(LocalTrade.bt_open_open_trade_count):
trade = self._enter_trade(pair, row, trade_dir) trade = self._enter_trade(pair, row, trade_dir)
if trade: if trade:
# TODO: hacky workaround to avoid opening > max_open_trades
# This emulates previous behavior - not sure if this is correct
# Prevents entering if the trade-slot was freed in this candle
open_trade_count_start += 1
self.wallets.update() self.wallets.update()
else: else:
self._collate_rejected(pair, row) self._collate_rejected(pair, row)
@ -1386,7 +1384,28 @@ class Backtesting:
order = trade.select_order(trade.exit_side, is_open=True) order = trade.select_order(trade.exit_side, is_open=True)
if order: if order:
self._process_exit_order(order, trade, current_time, row, pair) self._process_exit_order(order, trade, current_time, row, pair)
return open_trade_count_start
def time_pair_generator(
self, start_date: datetime, end_date: datetime, increment: timedelta, pairs: List[str]
):
"""
Backtest time and pair generator
"""
current_time = start_date + increment
self.progress.init_step(
BacktestState.BACKTEST, int((end_date - start_date) / self.timeframe_td)
)
while current_time <= end_date:
is_first = True
# Pairs that have open trades should be processed first
new_pairlist = list(dict.fromkeys([t.pair for t in LocalTrade.bt_trades_open] + pairs))
for pair in new_pairlist:
yield current_time, pair, is_first
is_first = False
self.progress.increment()
current_time += increment
def backtest(self, processed: Dict, start_date: datetime, end_date: datetime) -> Dict[str, Any]: def backtest(self, processed: Dict, start_date: datetime, end_date: datetime) -> Dict[str, Any]:
""" """
@ -1411,87 +1430,75 @@ class Backtesting:
# Indexes per pair, so some pairs are allowed to have a missing start. # Indexes per pair, so some pairs are allowed to have a missing start.
indexes: Dict = defaultdict(int) indexes: Dict = defaultdict(int)
current_time = start_date + self.timeframe_td
self.progress.init_step(
BacktestState.BACKTEST, int((end_date - start_date) / self.timeframe_td)
)
# Loop timerange and get candle for each pair at that point in time # Loop timerange and get candle for each pair at that point in time
while current_time <= end_date: for current_time, pair, is_first in self.time_pair_generator(
open_trade_count_start = LocalTrade.bt_open_open_trade_count start_date, end_date, self.timeframe_td, list(data.keys())
self.check_abort() ):
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)( if is_first:
current_time=current_time self.check_abort()
) strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
for i, pair in enumerate(data): current_time=current_time
row_index = indexes[pair] )
row = self.validate_row(data, pair, row_index, current_time) row_index = indexes[pair]
if not row: row = self.validate_row(data, pair, row_index, current_time)
if not row:
continue
row_index += 1
indexes[pair] = row_index
self.dataprovider._set_dataframe_max_index(self.required_startup + row_index)
self.dataprovider._set_dataframe_max_date(current_time)
current_detail_time: datetime = row[DATE_IDX].to_pydatetime()
trade_dir: Optional[LongShort] = self.check_for_trade_entry(row)
if (
(trade_dir is not None or len(LocalTrade.bt_trades_open_pp[pair]) > 0)
and self.timeframe_detail
and pair in self.detail_data
):
# Spread out into detail timeframe.
# Should only happen when we are either in a trade for this pair
# or when we got the signal for a new trade.
exit_candle_end = current_detail_time + self.timeframe_td
detail_data = self.detail_data[pair]
detail_data = detail_data.loc[
(detail_data["date"] >= current_detail_time)
& (detail_data["date"] < exit_candle_end)
].copy()
if len(detail_data) == 0:
# Fall back to "regular" data if no detail data was found for this candle
self.backtest_loop(row, pair, current_time, end_date, trade_dir)
continue continue
detail_data.loc[:, "enter_long"] = row[LONG_IDX]
row_index += 1 detail_data.loc[:, "exit_long"] = row[ELONG_IDX]
indexes[pair] = row_index detail_data.loc[:, "enter_short"] = row[SHORT_IDX]
self.dataprovider._set_dataframe_max_index(self.required_startup + row_index) detail_data.loc[:, "exit_short"] = row[ESHORT_IDX]
self.dataprovider._set_dataframe_max_date(current_time) detail_data.loc[:, "enter_tag"] = row[ENTER_TAG_IDX]
current_detail_time: datetime = row[DATE_IDX].to_pydatetime() detail_data.loc[:, "exit_tag"] = row[EXIT_TAG_IDX]
trade_dir: Optional[LongShort] = self.check_for_trade_entry(row) is_first = True
current_time_det = current_time
if ( for det_row in detail_data[HEADERS].values.tolist():
(trade_dir is not None or len(LocalTrade.bt_trades_open_pp[pair]) > 0) self.dataprovider._set_dataframe_max_date(current_time_det)
and self.timeframe_detail self.backtest_loop(
and pair in self.detail_data det_row,
): pair,
# Spread out into detail timeframe. current_time_det,
# Should only happen when we are either in a trade for this pair end_date,
# or when we got the signal for a new trade. trade_dir,
exit_candle_end = current_detail_time + self.timeframe_td is_first,
detail_data = self.detail_data[pair]
detail_data = detail_data.loc[
(detail_data["date"] >= current_detail_time)
& (detail_data["date"] < exit_candle_end)
].copy()
if len(detail_data) == 0:
# Fall back to "regular" data if no detail data was found for this candle
open_trade_count_start = self.backtest_loop(
row, pair, current_time, end_date, open_trade_count_start, trade_dir
)
continue
detail_data.loc[:, "enter_long"] = row[LONG_IDX]
detail_data.loc[:, "exit_long"] = row[ELONG_IDX]
detail_data.loc[:, "enter_short"] = row[SHORT_IDX]
detail_data.loc[:, "exit_short"] = row[ESHORT_IDX]
detail_data.loc[:, "enter_tag"] = row[ENTER_TAG_IDX]
detail_data.loc[:, "exit_tag"] = row[EXIT_TAG_IDX]
is_first = True
current_time_det = current_time
for det_row in detail_data[HEADERS].values.tolist():
self.dataprovider._set_dataframe_max_date(current_time_det)
open_trade_count_start = self.backtest_loop(
det_row,
pair,
current_time_det,
end_date,
open_trade_count_start,
trade_dir,
is_first,
)
current_time_det += self.timeframe_detail_td
is_first = False
else:
self.dataprovider._set_dataframe_max_date(current_time)
open_trade_count_start = self.backtest_loop(
row, pair, current_time, end_date, open_trade_count_start, trade_dir
) )
current_time_det += self.timeframe_detail_td
# Move time one configured time_interval ahead. is_first = False
self.progress.increment() else:
current_time += self.timeframe_td self.dataprovider._set_dataframe_max_date(current_time)
self.backtest_loop(row, pair, current_time, end_date, trade_dir)
self.handle_left_open(LocalTrade.bt_trades_open_pp, data=data) self.handle_left_open(LocalTrade.bt_trades_open_pp, data=data)
self.wallets.update() self.wallets.update()
results = trade_list_to_dataframe(LocalTrade.trades) results = trade_list_to_dataframe(LocalTrade.bt_trades)
return { return {
"results": results, "results": results,
"config": self.strategy.config, "config": self.strategy.config,

View File

@ -17,7 +17,6 @@ import rapidjson
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
from joblib.externals import cloudpickle from joblib.externals import cloudpickle
from pandas import DataFrame from pandas import DataFrame
from rich.align import Align
from rich.console import Console from rich.console import Console
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config
@ -80,7 +79,7 @@ class Hyperopt:
self.max_open_trades_space: List[Dimension] = [] self.max_open_trades_space: List[Dimension] = []
self.dimensions: List[Dimension] = [] self.dimensions: List[Dimension] = []
self._hyper_out: HyperoptOutput = HyperoptOutput() self._hyper_out: HyperoptOutput = HyperoptOutput(streaming=True)
self.config = config self.config = config
self.min_date: datetime self.min_date: datetime
@ -168,7 +167,9 @@ class Hyperopt:
cloudpickle.register_pickle_by_value(sys.modules[modules.__module__]) cloudpickle.register_pickle_by_value(sys.modules[modules.__module__])
self.hyperopt_pickle_magic(modules.__bases__) self.hyperopt_pickle_magic(modules.__bases__)
def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict: def _get_params_dict(
self, dimensions: List[Dimension], raw_params: List[Any]
) -> Dict[str, Any]:
# Ensure the number of dimensions match # Ensure the number of dimensions match
# the number of parameters in the list. # the number of parameters in the list.
if len(raw_params) != len(dimensions): if len(raw_params) != len(dimensions):
@ -317,7 +318,7 @@ class Hyperopt:
+ self.max_open_trades_space + self.max_open_trades_space
) )
def assign_params(self, params_dict: Dict, category: str) -> None: def assign_params(self, params_dict: Dict[str, Any], category: str) -> None:
""" """
Assign hyperoptable parameters Assign hyperoptable parameters
""" """
@ -404,7 +405,12 @@ class Hyperopt:
) )
def _get_results_dict( def _get_results_dict(
self, backtesting_results, min_date, max_date, params_dict, processed: Dict[str, DataFrame] self,
backtesting_results: Dict[str, Any],
min_date: datetime,
max_date: datetime,
params_dict: Dict[str, Any],
processed: Dict[str, DataFrame],
) -> Dict[str, Any]: ) -> Dict[str, Any]:
params_details = self._get_params_details(params_dict) params_details = self._get_params_details(params_dict)
@ -628,7 +634,7 @@ class Hyperopt:
# Define progressbar # Define progressbar
with get_progress_tracker( with get_progress_tracker(
console=console, console=console,
cust_objs=[Align.center(self._hyper_out.table)], cust_callables=[self._hyper_out],
) as pbar: ) as pbar:
task = pbar.add_task("Epochs", total=self.total_epochs) task = pbar.add_task("Epochs", total=self.total_epochs)

View File

@ -1,6 +1,8 @@
import sys import sys
from typing import List, Optional, Union from os import get_terminal_size
from typing import Any, List, Optional
from rich.align import Align
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
@ -11,7 +13,16 @@ from freqtrade.util import fmt_coin
class HyperoptOutput: class HyperoptOutput:
def __init__(self): def __init__(self, streaming=False) -> None:
self._results: List[Any] = []
self._streaming = streaming
self.__init_table()
def __call__(self, *args: Any, **kwds: Any) -> Any:
return Align.center(self.table)
def __init_table(self) -> None:
"""Initialize table"""
self.table = Table( self.table = Table(
title="Hyperopt results", title="Hyperopt results",
) )
@ -26,17 +37,6 @@ class HyperoptOutput:
self.table.add_column("Objective", justify="right") self.table.add_column("Objective", justify="right")
self.table.add_column("Max Drawdown (Acct)", justify="right") self.table.add_column("Max Drawdown (Acct)", justify="right")
def _add_row(self, data: List[Union[str, Text]]):
"""Add single row"""
row_to_add: List[Union[str, Text]] = [r if isinstance(r, Text) else str(r) for r in data]
self.table.add_row(*row_to_add)
def _add_rows(self, data: List[List[Union[str, Text]]]):
"""add multiple rows"""
for row in data:
self._add_row(row)
def print(self, console: Optional[Console] = None, *, print_colorized=True): def print(self, console: Optional[Console] = None, *, print_colorized=True):
if not console: if not console:
console = Console( console = Console(
@ -55,8 +55,28 @@ class HyperoptOutput:
) -> None: ) -> None:
"""Format one or multiple rows and add them""" """Format one or multiple rows and add them"""
stake_currency = config["stake_currency"] stake_currency = config["stake_currency"]
self._results.extend(results)
for r in results: max_rows: Optional[int] = None
if self._streaming:
try:
ts = get_terminal_size()
# Get terminal size.
# Account for header, borders, and for the progress bar.
# This assumes that lines don't wrap.
if ts.columns < 148:
# If the terminal is too small, we can't display the table properly.
# We will halve the number of rows to display.
max_rows = -(int(ts.lines / 2) - 6)
else:
max_rows = -(ts.lines - 6)
except OSError:
# If we can't get the terminal size, we will just display the last 10 rows.
pass
self.__init_table()
for r in self._results[max_rows:]:
self.table.add_row( self.table.add_row(
*[ *[
# "Best": # "Best":

View File

@ -147,6 +147,9 @@ def migrate_trades_and_orders_table(
price_precision = get_column_def(cols, "price_precision", "null") price_precision = get_column_def(cols, "price_precision", "null")
precision_mode = get_column_def(cols, "precision_mode", "null") precision_mode = get_column_def(cols, "precision_mode", "null")
contract_size = get_column_def(cols, "contract_size", "null") contract_size = get_column_def(cols, "contract_size", "null")
precision_mode_price = get_column_def(
cols, "precision_mode_price", get_column_def(cols, "precision_mode", "null")
)
# Schema migration necessary # Schema migration necessary
with engine.begin() as connection: with engine.begin() as connection:
@ -177,7 +180,7 @@ def migrate_trades_and_orders_table(
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, funding_fee_running, realized_profit, interest_rate, funding_fees, funding_fee_running, realized_profit,
amount_precision, price_precision, precision_mode, contract_size, amount_precision, price_precision, precision_mode, precision_mode_price, contract_size,
max_stake_amount max_stake_amount
) )
select id, lower(exchange), pair, {base_currency} base_currency, select id, lower(exchange), pair, {base_currency} base_currency,
@ -207,8 +210,8 @@ def migrate_trades_and_orders_table(
{funding_fees} funding_fees, {funding_fee_running} funding_fee_running, {funding_fees} funding_fees, {funding_fee_running} funding_fee_running,
{realized_profit} realized_profit, {realized_profit} realized_profit,
{amount_precision} amount_precision, {price_precision} price_precision, {amount_precision} amount_precision, {price_precision} price_precision,
{precision_mode} precision_mode, {contract_size} contract_size, {precision_mode} precision_mode, {precision_mode_price} precision_mode_price,
{max_stake_amount} max_stake_amount {contract_size} contract_size, {max_stake_amount} max_stake_amount
from {trade_back_name} from {trade_back_name}
""" """
) )
@ -348,8 +351,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# if ('orders' not in previous_tables # if ('orders' not in previous_tables
# or not has_column(cols_orders, 'funding_fee')): # or not has_column(cols_orders, 'funding_fee')):
migrating = False migrating = False
# if not has_column(cols_trades, 'funding_fee_running'): if not has_column(cols_trades, "precision_mode_price"):
if not has_column(cols_orders, "ft_order_tag"): # if not has_column(cols_orders, "ft_order_tag"):
migrating = True migrating = True
logger.info( logger.info(
f"Running database migration for trades - " f"Running database migration for trades - "

View File

@ -373,12 +373,12 @@ class LocalTrade:
use_db: bool = False use_db: bool = False
# Trades container for backtesting # Trades container for backtesting
trades: List["LocalTrade"] = [] bt_trades: List["LocalTrade"] = []
trades_open: List["LocalTrade"] = [] bt_trades_open: List["LocalTrade"] = []
# Copy of trades_open - but indexed by pair # Copy of trades_open - but indexed by pair
bt_trades_open_pp: Dict[str, List["LocalTrade"]] = defaultdict(list) bt_trades_open_pp: Dict[str, List["LocalTrade"]] = defaultdict(list)
bt_open_open_trade_count: int = 0 bt_open_open_trade_count: int = 0
total_profit: float = 0 bt_total_profit: float = 0
realized_profit: float = 0 realized_profit: float = 0
id: int = 0 id: int = 0
@ -433,6 +433,7 @@ class LocalTrade:
amount_precision: Optional[float] = None amount_precision: Optional[float] = None
price_precision: Optional[float] = None price_precision: Optional[float] = None
precision_mode: Optional[int] = None precision_mode: Optional[int] = None
precision_mode_price: Optional[int] = None
contract_size: Optional[float] = None contract_size: Optional[float] = None
# Leverage trading properties # Leverage trading properties
@ -730,6 +731,7 @@ class LocalTrade:
"amount_precision": self.amount_precision, "amount_precision": self.amount_precision,
"price_precision": self.price_precision, "price_precision": self.price_precision,
"precision_mode": self.precision_mode, "precision_mode": self.precision_mode,
"precision_mode_price": self.precision_mode_price,
"contract_size": self.contract_size, "contract_size": self.contract_size,
"has_open_orders": self.has_open_orders, "has_open_orders": self.has_open_orders,
"orders": orders_json, "orders": orders_json,
@ -740,11 +742,11 @@ class LocalTrade:
""" """
Resets all trades. Only active for backtesting mode. Resets all trades. Only active for backtesting mode.
""" """
LocalTrade.trades = [] LocalTrade.bt_trades = []
LocalTrade.trades_open = [] LocalTrade.bt_trades_open = []
LocalTrade.bt_trades_open_pp = defaultdict(list) LocalTrade.bt_trades_open_pp = defaultdict(list)
LocalTrade.bt_open_open_trade_count = 0 LocalTrade.bt_open_open_trade_count = 0
LocalTrade.total_profit = 0 LocalTrade.bt_total_profit = 0
def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None: def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None:
""" """
@ -810,7 +812,7 @@ class LocalTrade:
stop_loss_norm = price_to_precision( stop_loss_norm = price_to_precision(
new_loss, new_loss,
self.price_precision, self.price_precision,
self.precision_mode, self.precision_mode_price,
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP, rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP,
) )
# no stop loss assigned yet # no stop loss assigned yet
@ -819,7 +821,7 @@ class LocalTrade:
self.initial_stop_loss = price_to_precision( self.initial_stop_loss = price_to_precision(
stop_loss_norm, stop_loss_norm,
self.price_precision, self.price_precision,
self.precision_mode, self.precision_mode_price,
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP, rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP,
) )
self.initial_stop_loss_pct = -1 * abs(stoploss) self.initial_stop_loss_pct = -1 * abs(stoploss)
@ -1217,7 +1219,7 @@ class LocalTrade:
# with realized_profit. # with realized_profit.
close_profit = (close_profit_abs / total_stake) * self.leverage close_profit = (close_profit_abs / total_stake) * self.leverage
else: else:
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price) total_stake += self._calc_open_trade_value(tmp_amount, price)
max_stake_amount += tmp_amount * price max_stake_amount += tmp_amount * price
self.funding_fees = funding_fees self.funding_fees = funding_fees
self.max_stake_amount = float(max_stake_amount) self.max_stake_amount = float(max_stake_amount)
@ -1236,7 +1238,7 @@ class LocalTrade:
self.open_rate = float(current_stake / current_amount) self.open_rate = float(current_stake / current_amount)
self.amount = current_amount_tr self.amount = current_amount_tr
self.stake_amount = float(current_stake) / (self.leverage or 1.0) self.stake_amount = float(current_stake) / (self.leverage or 1.0)
self.fee_open_cost = self.fee_open * float(current_stake) self.fee_open_cost = self.fee_open * float(self.max_stake_amount)
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)
@ -1405,7 +1407,7 @@ class LocalTrade:
Helper function to query Trades. Helper function to query Trades.
Returns a List of trades, filtered on the parameters given. Returns a List of trades, filtered on the parameters given.
In live mode, converts the filter to a database query and returns all rows In live mode, converts the filter to a database query and returns all rows
In Backtest mode, uses filters on Trade.trades to get the result. In Backtest mode, uses filters on Trade.bt_trades to get the result.
:param pair: Filter by pair :param pair: Filter by pair
:param is_open: Filter by open/closed status :param is_open: Filter by open/closed status
@ -1418,13 +1420,13 @@ class LocalTrade:
# Offline mode - without database # Offline mode - without database
if is_open is not None: if is_open is not None:
if is_open: if is_open:
sel_trades = LocalTrade.trades_open sel_trades = LocalTrade.bt_trades_open
else: else:
sel_trades = LocalTrade.trades sel_trades = LocalTrade.bt_trades
else: else:
# Not used during backtesting, but might be used by a strategy # Not used during backtesting, but might be used by a strategy
sel_trades = list(LocalTrade.trades + LocalTrade.trades_open) sel_trades = list(LocalTrade.bt_trades + LocalTrade.bt_trades_open)
if pair: if pair:
sel_trades = [trade for trade in sel_trades if trade.pair == pair] sel_trades = [trade for trade in sel_trades if trade.pair == pair]
@ -1439,24 +1441,24 @@ class LocalTrade:
@staticmethod @staticmethod
def close_bt_trade(trade): def close_bt_trade(trade):
LocalTrade.trades_open.remove(trade) LocalTrade.bt_trades_open.remove(trade)
LocalTrade.bt_trades_open_pp[trade.pair].remove(trade) LocalTrade.bt_trades_open_pp[trade.pair].remove(trade)
LocalTrade.bt_open_open_trade_count -= 1 LocalTrade.bt_open_open_trade_count -= 1
LocalTrade.trades.append(trade) LocalTrade.bt_trades.append(trade)
LocalTrade.total_profit += trade.close_profit_abs LocalTrade.bt_total_profit += trade.close_profit_abs
@staticmethod @staticmethod
def add_bt_trade(trade): def add_bt_trade(trade):
if trade.is_open: if trade.is_open:
LocalTrade.trades_open.append(trade) LocalTrade.bt_trades_open.append(trade)
LocalTrade.bt_trades_open_pp[trade.pair].append(trade) LocalTrade.bt_trades_open_pp[trade.pair].append(trade)
LocalTrade.bt_open_open_trade_count += 1 LocalTrade.bt_open_open_trade_count += 1
else: else:
LocalTrade.trades.append(trade) LocalTrade.bt_trades.append(trade)
@staticmethod @staticmethod
def remove_bt_trade(trade): def remove_bt_trade(trade):
LocalTrade.trades_open.remove(trade) LocalTrade.bt_trades_open.remove(trade)
LocalTrade.bt_trades_open_pp[trade.pair].remove(trade) LocalTrade.bt_trades_open_pp[trade.pair].remove(trade)
LocalTrade.bt_open_open_trade_count -= 1 LocalTrade.bt_open_open_trade_count -= 1
@ -1562,6 +1564,7 @@ class LocalTrade:
amount_precision=data.get("amount_precision", None), amount_precision=data.get("amount_precision", None),
price_precision=data.get("price_precision", None), price_precision=data.get("price_precision", None),
precision_mode=data.get("precision_mode", None), precision_mode=data.get("precision_mode", None),
precision_mode_price=data.get("precision_mode_price", data.get("precision_mode", None)),
contract_size=data.get("contract_size", None), contract_size=data.get("contract_size", None),
) )
for order in data["orders"]: for order in data["orders"]:
@ -1695,6 +1698,9 @@ class Trade(ModelBase, LocalTrade):
) )
price_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore price_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore
precision_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore precision_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore
precision_mode_price: Mapped[Optional[int]] = mapped_column( # type: ignore
Integer, nullable=True
)
contract_size: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore contract_size: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore
# Leverage trading properties # Leverage trading properties
@ -1761,7 +1767,7 @@ class Trade(ModelBase, LocalTrade):
Helper function to query Trades.j Helper function to query Trades.j
Returns a List of trades, filtered on the parameters given. Returns a List of trades, filtered on the parameters given.
In live mode, converts the filter to a database query and returns all rows In live mode, converts the filter to a database query and returns all rows
In Backtest mode, uses filters on Trade.trades to get the result. In Backtest mode, uses filters on Trade.bt_trades to get the result.
:return: unsorted List[Trade] :return: unsorted List[Trade]
""" """

View File

@ -0,0 +1,329 @@
"""
Percent Change PairList provider
Provides dynamic pair list based on trade change
sorted based on percentage change in price over a
defined period or as coming from ticker
"""
import logging
from datetime import timedelta
from typing import Any, Dict, List, Optional
from cachetools import TTLCache
from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.exchange.types import Ticker, Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
from freqtrade.util import dt_now, format_ms_time
logger = logging.getLogger(__name__)
class PercentChangePairList(IPairList):
is_pairlist_generator = True
supports_backtesting = SupportsBacktesting.NO
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if "number_assets" not in self._pairlistconfig:
raise OperationalException(
"`number_assets` not specified. Please check your configuration "
'for "pairlist.config.number_assets"'
)
self._stake_currency = self._config["stake_currency"]
self._number_pairs = self._pairlistconfig["number_assets"]
self._min_value = self._pairlistconfig.get("min_value", None)
self._max_value = self._pairlistconfig.get("max_value", None)
self._refresh_period = self._pairlistconfig.get("refresh_period", 1800)
self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
self._lookback_days = self._pairlistconfig.get("lookback_days", 0)
self._lookback_timeframe = self._pairlistconfig.get("lookback_timeframe", "1d")
self._lookback_period = self._pairlistconfig.get("lookback_period", 0)
self._sort_direction: Optional[str] = self._pairlistconfig.get("sort_direction", "desc")
self._def_candletype = self._config["candle_type_def"]
if (self._lookback_days > 0) & (self._lookback_period > 0):
raise OperationalException(
"Ambiguous configuration: lookback_days and lookback_period both set in pairlist "
"config. Please set lookback_days only or lookback_period and lookback_timeframe "
"and restart the bot."
)
# overwrite lookback timeframe and days when lookback_days is set
if self._lookback_days > 0:
self._lookback_timeframe = "1d"
self._lookback_period = self._lookback_days
# get timeframe in minutes and seconds
self._tf_in_min = timeframe_to_minutes(self._lookback_timeframe)
_tf_in_sec = self._tf_in_min * 60
# whether to use range lookback or not
self._use_range = (self._tf_in_min > 0) & (self._lookback_period > 0)
if self._use_range & (self._refresh_period < _tf_in_sec):
raise OperationalException(
f"Refresh period of {self._refresh_period} seconds is smaller than one "
f"timeframe of {self._lookback_timeframe}. Please adjust refresh_period "
f"to at least {_tf_in_sec} and restart the bot."
)
if not self._use_range and not (
self._exchange.exchange_has("fetchTickers")
and self._exchange.get_option("tickers_have_percentage")
):
raise OperationalException(
"Exchange does not support dynamic whitelist in this configuration. "
"Please edit your config and either remove PercentChangePairList, "
"or switch to using candles. and restart the bot."
)
candle_limit = self._exchange.ohlcv_candle_limit(
self._lookback_timeframe, self._config["candle_type_def"]
)
if self._lookback_period > candle_limit:
raise OperationalException(
"ChangeFilter requires lookback_period to not "
f"exceed exchange max request size ({candle_limit})"
)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return not self._use_range
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
return f"{self.name} - top {self._pairlistconfig['number_assets']} percent change pairs."
@staticmethod
def description() -> str:
return "Provides dynamic pair list based on percentage change."
@staticmethod
def available_parameters() -> Dict[str, PairlistParameter]:
return {
"number_assets": {
"type": "number",
"default": 30,
"description": "Number of assets",
"help": "Number of assets to use from the pairlist",
},
"min_value": {
"type": "number",
"default": None,
"description": "Minimum value",
"help": "Minimum value to use for filtering the pairlist.",
},
"max_value": {
"type": "number",
"default": None,
"description": "Maximum value",
"help": "Maximum value to use for filtering the pairlist.",
},
"sort_direction": {
"type": "option",
"default": "desc",
"options": ["", "asc", "desc"],
"description": "Sort pairlist",
"help": "Sort Pairlist ascending or descending by rate of change.",
},
**IPairList.refresh_period_parameter(),
"lookback_days": {
"type": "number",
"default": 0,
"description": "Lookback Days",
"help": "Number of days to look back at.",
},
"lookback_timeframe": {
"type": "string",
"default": "1d",
"description": "Lookback Timeframe",
"help": "Timeframe to use for lookback.",
},
"lookback_period": {
"type": "number",
"default": 0,
"description": "Lookback Period",
"help": "Number of periods to look back at.",
},
}
def gen_pairlist(self, tickers: Tickers) -> List[str]:
"""
Generate the pairlist
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: List of pairs
"""
pairlist = self._pair_cache.get("pairlist")
if pairlist:
# Item found - no refresh necessary
return pairlist.copy()
else:
# Use fresh pairlist
# Check if pair quote currency equals to the stake currency.
_pairlist = [
k
for k in self._exchange.get_markets(
quote_currencies=[self._stake_currency], tradable_only=True, active_only=True
).keys()
]
# No point in testing for blacklisted pairs...
_pairlist = self.verify_blacklist(_pairlist, logger.info)
if not self._use_range:
filtered_tickers = [
v
for k, v in tickers.items()
if (
self._exchange.get_pair_quote_currency(k) == self._stake_currency
and (self._use_range or v.get("percentage") is not None)
and v["symbol"] in _pairlist
)
]
pairlist = [s["symbol"] for s in filtered_tickers]
else:
pairlist = _pairlist
pairlist = self.filter_pairlist(pairlist, tickers)
self._pair_cache["pairlist"] = pairlist.copy()
return pairlist
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: new whitelist
"""
filtered_tickers: List[Dict[str, Any]] = [{"symbol": k} for k in pairlist]
if self._use_range:
# calculating using lookback_period
self.fetch_percent_change_from_lookback_period(filtered_tickers)
else:
# Fetching 24h change by default from supported exchange tickers
self.fetch_percent_change_from_tickers(filtered_tickers, tickers)
if self._min_value is not None:
filtered_tickers = [v for v in filtered_tickers if v["percentage"] > self._min_value]
if self._max_value is not None:
filtered_tickers = [v for v in filtered_tickers if v["percentage"] < self._max_value]
sorted_tickers = sorted(
filtered_tickers,
reverse=self._sort_direction == "desc",
key=lambda t: t["percentage"],
)
# Validate whitelist to only have active market pairs
pairs = self._whitelist_for_active_markets([s["symbol"] for s in sorted_tickers])
pairs = self.verify_blacklist(pairs, logmethod=logger.info)
# Limit pairlist to the requested number of pairs
pairs = pairs[: self._number_pairs]
return pairs
def fetch_candles_for_lookback_period(
self, filtered_tickers: List[Dict[str, str]]
) -> Dict[PairWithTimeframe, DataFrame]:
since_ms = (
int(
timeframe_to_prev_date(
self._lookback_timeframe,
dt_now()
+ timedelta(
minutes=-(self._lookback_period * self._tf_in_min) - self._tf_in_min
),
).timestamp()
)
* 1000
)
to_ms = (
int(
timeframe_to_prev_date(
self._lookback_timeframe, dt_now() - timedelta(minutes=self._tf_in_min)
).timestamp()
)
* 1000
)
# todo: utc date output for starting date
self.log_once(
f"Using change range of {self._lookback_period} candles, timeframe: "
f"{self._lookback_timeframe}, starting from {format_ms_time(since_ms)} "
f"till {format_ms_time(to_ms)}",
logger.info,
)
needed_pairs: ListPairsWithTimeframes = [
(p, self._lookback_timeframe, self._def_candletype)
for p in [s["symbol"] for s in filtered_tickers]
if p not in self._pair_cache
]
candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms)
return candles
def fetch_percent_change_from_lookback_period(self, filtered_tickers: List[Dict[str, Any]]):
# get lookback period in ms, for exchange ohlcv fetch
candles = self.fetch_candles_for_lookback_period(filtered_tickers)
for i, p in enumerate(filtered_tickers):
pair_candles = (
candles[(p["symbol"], self._lookback_timeframe, self._def_candletype)]
if (p["symbol"], self._lookback_timeframe, self._def_candletype) in candles
else None
)
# in case of candle data calculate typical price and change for candle
if pair_candles is not None and not pair_candles.empty:
current_close = pair_candles["close"].iloc[-1]
previous_close = pair_candles["close"].shift(self._lookback_period).iloc[-1]
pct_change = (
((current_close - previous_close) / previous_close) if previous_close > 0 else 0
)
# replace change with a range change sum calculated above
filtered_tickers[i]["percentage"] = pct_change
else:
filtered_tickers[i]["percentage"] = 0
def fetch_percent_change_from_tickers(self, filtered_tickers: List[Dict[str, Any]], tickers):
for i, p in enumerate(filtered_tickers):
# Filter out assets
if not self._validate_pair(
p["symbol"], tickers[p["symbol"]] if p["symbol"] in tickers else None
):
filtered_tickers.remove(p)
else:
filtered_tickers[i]["percentage"] = tickers[p["symbol"]]["percentage"]
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
"""
Check if one price-step (pip) is > than a certain barrier.
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.fetch_ticker
:return: True if the pair can stay, false if it should be removed
"""
if not ticker or "percentage" not in ticker or ticker["percentage"] is None:
self.log_once(
f"Removed {pair} from whitelist, because "
"ticker['percentage'] is empty (Usually no trade in the last 24h).",
logger.info,
)
return False
return True

View File

@ -18,19 +18,19 @@ class CooldownPeriod(IProtection):
""" """
LockReason to use LockReason to use
""" """
return f"Cooldown period for {self.stop_duration_str}." return f"Cooldown period for {self.unlock_reason_time_element}."
def short_desc(self) -> str: def short_desc(self) -> str:
""" """
Short method description - used for startup-messages Short method description - used for startup messages
""" """
return f"{self.name} - Cooldown period of {self.stop_duration_str}." return f"{self.name} - Cooldown period {self.unlock_reason_time_element}."
def _cooldown_period(self, pair: str, date_now: datetime) -> Optional[ProtectionReturn]: def _cooldown_period(self, pair: str, date_now: datetime) -> Optional[ProtectionReturn]:
""" """
Get last trade for this pair Get last trade for this pair
""" """
look_back_until = date_now - timedelta(minutes=self._stop_duration) look_back_until = date_now - timedelta(minutes=self._lookback_period)
# filters = [ # filters = [
# Trade.is_open.is_(False), # Trade.is_open.is_(False),
# Trade.close_date > look_back_until, # Trade.close_date > look_back_until,
@ -42,8 +42,8 @@ class CooldownPeriod(IProtection):
# Get latest trade # Get latest trade
# Ignore type error as we know we only get closed trades. # Ignore type error as we know we only get closed trades.
trade = sorted(trades, key=lambda t: t.close_date)[-1] # type: ignore trade = sorted(trades, key=lambda t: t.close_date)[-1] # type: ignore
self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info) self.log_once(f"Cooldown for {pair} {self.unlock_reason_time_element}.", logger.info)
until = self.calculate_lock_end([trade], self._stop_duration) until = self.calculate_lock_end([trade])
return ProtectionReturn( return ProtectionReturn(
lock=True, lock=True,

View File

@ -32,15 +32,19 @@ class IProtection(LoggingMixin, ABC):
self._config = config self._config = config
self._protection_config = protection_config self._protection_config = protection_config
self._stop_duration_candles: Optional[int] = None self._stop_duration_candles: Optional[int] = None
self._stop_duration: int = 0
self._lookback_period_candles: Optional[int] = None self._lookback_period_candles: Optional[int] = None
self._unlock_at: Optional[str] = None
tf_in_min = timeframe_to_minutes(config["timeframe"]) tf_in_min = timeframe_to_minutes(config["timeframe"])
if "stop_duration_candles" in protection_config: if "stop_duration_candles" in protection_config:
self._stop_duration_candles = int(protection_config.get("stop_duration_candles", 1)) self._stop_duration_candles = int(protection_config.get("stop_duration_candles", 1))
self._stop_duration = tf_in_min * self._stop_duration_candles self._stop_duration = tf_in_min * self._stop_duration_candles
elif "unlock_at" in protection_config:
self._unlock_at = protection_config.get("unlock_at")
else: else:
self._stop_duration_candles = None
self._stop_duration = int(protection_config.get("stop_duration", 60)) self._stop_duration = int(protection_config.get("stop_duration", 60))
if "lookback_period_candles" in protection_config: if "lookback_period_candles" in protection_config:
self._lookback_period_candles = int(protection_config.get("lookback_period_candles", 1)) self._lookback_period_candles = int(protection_config.get("lookback_period_candles", 1))
self._lookback_period = tf_in_min * self._lookback_period_candles self._lookback_period = tf_in_min * self._lookback_period_candles
@ -80,6 +84,16 @@ class IProtection(LoggingMixin, ABC):
else: else:
return f"{self._lookback_period} {plural(self._lookback_period, 'minute', 'minutes')}" return f"{self._lookback_period} {plural(self._lookback_period, 'minute', 'minutes')}"
@property
def unlock_reason_time_element(self) -> str:
"""
Output configured unlock time or stop duration
"""
if self._unlock_at is not None:
return f"until {self._unlock_at}"
else:
return f"for {self.stop_duration_str}"
@abstractmethod @abstractmethod
def short_desc(self) -> str: def short_desc(self) -> str:
""" """
@ -105,16 +119,23 @@ class IProtection(LoggingMixin, ABC):
If true, this pair will be locked with <reason> until <until> If true, this pair will be locked with <reason> until <until>
""" """
@staticmethod def calculate_lock_end(self, trades: List[LocalTrade]) -> datetime:
def calculate_lock_end(trades: List[LocalTrade], stop_minutes: int) -> datetime:
""" """
Get lock end time Get lock end time
Implicitly uses `self._stop_duration` or `self._unlock_at` depending on the configuration.
""" """
max_date: datetime = max([trade.close_date for trade in trades if trade.close_date]) max_date: datetime = max([trade.close_date for trade in trades if trade.close_date])
# coming from Database, tzinfo is not set. # coming from Database, tzinfo is not set.
if max_date.tzinfo is None: if max_date.tzinfo is None:
max_date = max_date.replace(tzinfo=timezone.utc) max_date = max_date.replace(tzinfo=timezone.utc)
until = max_date + timedelta(minutes=stop_minutes) if self._unlock_at is not None:
# unlock_at case with fixed hour of the day
hour, minutes = self._unlock_at.split(":")
unlock_at = max_date.replace(hour=int(hour), minute=int(minutes))
if unlock_at < max_date:
unlock_at += timedelta(days=1)
return unlock_at
until = max_date + timedelta(minutes=self._stop_duration)
return until return until

View File

@ -36,7 +36,7 @@ class LowProfitPairs(IProtection):
""" """
return ( return (
f"{profit} < {self._required_profit} in {self.lookback_period_str}, " f"{profit} < {self._required_profit} in {self.lookback_period_str}, "
f"locking for {self.stop_duration_str}." f"locking {self.unlock_reason_time_element}."
) )
def _low_profit( def _low_profit(
@ -70,7 +70,7 @@ class LowProfitPairs(IProtection):
f"within {self._lookback_period} minutes.", f"within {self._lookback_period} minutes.",
logger.info, logger.info,
) )
until = self.calculate_lock_end(trades, self._stop_duration) until = self.calculate_lock_end(trades)
return ProtectionReturn( return ProtectionReturn(
lock=True, lock=True,

View File

@ -39,7 +39,7 @@ class MaxDrawdown(IProtection):
""" """
return ( return (
f"{drawdown} passed {self._max_allowed_drawdown} in {self.lookback_period_str}, " f"{drawdown} passed {self._max_allowed_drawdown} in {self.lookback_period_str}, "
f"locking for {self.stop_duration_str}." f"locking {self.unlock_reason_time_element}."
) )
def _max_drawdown(self, date_now: datetime) -> Optional[ProtectionReturn]: def _max_drawdown(self, date_now: datetime) -> Optional[ProtectionReturn]:
@ -70,7 +70,8 @@ class MaxDrawdown(IProtection):
f" within {self.lookback_period_str}.", f" within {self.lookback_period_str}.",
logger.info, logger.info,
) )
until = self.calculate_lock_end(trades, self._stop_duration)
until = self.calculate_lock_end(trades)
return ProtectionReturn( return ProtectionReturn(
lock=True, lock=True,

View File

@ -38,7 +38,7 @@ class StoplossGuard(IProtection):
""" """
return ( return (
f"{self._trade_limit} stoplosses in {self._lookback_period} min, " f"{self._trade_limit} stoplosses in {self._lookback_period} min, "
f"locking for {self._stop_duration} min." f"locking {self.unlock_reason_time_element}."
) )
def _stoploss_guard( def _stoploss_guard(
@ -78,7 +78,7 @@ class StoplossGuard(IProtection):
f"stoplosses within {self._lookback_period} minutes.", f"stoplosses within {self._lookback_period} minutes.",
logger.info, logger.info,
) )
until = self.calculate_lock_end(trades, self._stop_duration) until = self.calculate_lock_end(trades)
return ProtectionReturn( return ProtectionReturn(
lock=True, lock=True,
until=until, until=until,

View File

@ -182,7 +182,7 @@ def api_get_backtest():
ApiBG.bt["bt"].progress.action if ApiBG.bt["bt"] else str(BacktestState.STARTUP) ApiBG.bt["bt"].progress.action if ApiBG.bt["bt"] else str(BacktestState.STARTUP)
), ),
"progress": ApiBG.bt["bt"].progress.progress if ApiBG.bt["bt"] else 0, "progress": ApiBG.bt["bt"].progress.progress if ApiBG.bt["bt"] else 0,
"trade_count": len(LocalTrade.trades), "trade_count": len(LocalTrade.bt_trades),
"status_msg": "Backtest running", "status_msg": "Backtest running",
} }

View File

@ -67,7 +67,6 @@ class Balance(BaseModel):
stake: str stake: str
# Starting with 2.x # Starting with 2.x
side: str side: str
leverage: float
is_position: bool is_position: bool
position: float position: float
is_bot_managed: bool is_bot_managed: bool

View File

@ -58,7 +58,7 @@ async def channel_broadcaster(channel: WebSocketChannel, message_stream: Message
" consumers." " consumers."
) )
await channel.send(message, timeout=True) await channel.send(message, use_timeout=True)
async def _process_consumer_request(request: Dict[str, Any], channel: WebSocketChannel, rpc: RPC): async def _process_consumer_request(request: Dict[str, Any], channel: WebSocketChannel, rpc: RPC):

View File

@ -8,6 +8,7 @@ def asyncio_setup() -> None: # pragma: no cover
# Set eventloop for win32 setups # Set eventloop for win32 setups
# Reverts a change done in uvicorn 0.15.0 - which now sets the eventloop # Reverts a change done in uvicorn 0.15.0 - which now sets the eventloop
# via policy. # via policy.
# TODO: is this workaround actually needed?
import sys import sys
if sys.version_info >= (3, 8) and sys.platform == "win32": if sys.version_info >= (3, 8) and sys.platform == "win32":

View File

@ -80,7 +80,7 @@ class WebSocketChannel:
self._send_high_limit = min(max(self.avg_send_time * 2, 1), 3) self._send_high_limit = min(max(self.avg_send_time * 2, 1), 3)
async def send( async def send(
self, message: Union[WSMessageSchemaType, Dict[str, Any]], timeout: bool = False self, message: Union[WSMessageSchemaType, Dict[str, Any]], use_timeout: bool = False
): ):
""" """
Send a message on the wrapped websocket. If the sending Send a message on the wrapped websocket. If the sending
@ -88,7 +88,7 @@ class WebSocketChannel:
disconnect the connection. disconnect the connection.
:param message: The message to send :param message: The message to send
:param timeout: Enforce send high limit, defaults to False :param use_timeout: Enforce send high limit, defaults to False
""" """
try: try:
_ = time.time() _ = time.time()
@ -96,7 +96,8 @@ class WebSocketChannel:
# a TimeoutError and bubble up to the # a TimeoutError and bubble up to the
# message_endpoint to close the connection # message_endpoint to close the connection
await asyncio.wait_for( await asyncio.wait_for(
self._wrapped_ws.send(message), timeout=self._send_high_limit if timeout else None self._wrapped_ws.send(message),
timeout=self._send_high_limit if use_timeout else None,
) )
total_time = time.time() - _ total_time = time.time() - _
self._send_times.append(total_time) self._send_times.append(total_time)

View File

@ -296,7 +296,10 @@ class RPC:
else: else:
trade_profit = 0.0 trade_profit = 0.0
profit_str = f"{0.0:.2f}" profit_str = f"{0.0:.2f}"
direction_str = ("S" if trade.is_short else "L") if nonspot else "" leverage = f"{trade.leverage:.3g}"
direction_str = (
(f"S {leverage}x" if trade.is_short else f"L {leverage}x") if nonspot else ""
)
if self._fiat_converter: if self._fiat_converter:
fiat_profit = self._fiat_converter.convert_amount( fiat_profit = self._fiat_converter.convert_amount(
trade_profit, stake_currency, fiat_display_currency trade_profit, stake_currency, fiat_display_currency
@ -742,7 +745,6 @@ class RPC:
"est_stake_bot": est_stake_bot if is_bot_managed else 0, "est_stake_bot": est_stake_bot if is_bot_managed else 0,
"stake": stake_currency, "stake": stake_currency,
"side": "long", "side": "long",
"leverage": 1,
"position": 0, "position": 0,
"is_bot_managed": is_bot_managed, "is_bot_managed": is_bot_managed,
"is_position": False, "is_position": False,
@ -764,7 +766,6 @@ class RPC:
"est_stake": position.collateral, "est_stake": position.collateral,
"est_stake_bot": position.collateral, "est_stake_bot": position.collateral,
"stake": stake_currency, "stake": stake_currency,
"leverage": position.leverage,
"side": position.side, "side": position.side,
"is_bot_managed": True, "is_bot_managed": True,
"is_position": True, "is_position": True,

View File

@ -1133,7 +1133,6 @@ class Telegram(RPCHandler):
curr_output = ( curr_output = (
f"*{curr['currency']}:*\n" f"*{curr['currency']}:*\n"
f"\t`{curr['side']}: {curr['position']:.8f}`\n" f"\t`{curr['side']}: {curr['position']:.8f}`\n"
f"\t`Leverage: {curr['leverage']:.1f}`\n"
f"\t`Est. {curr['stake']}: " f"\t`Est. {curr['stake']}: "
f"{fmt_coin(curr['est_stake'], curr['stake'], False)}`\n" f"{fmt_coin(curr['est_stake'], curr['stake'], False)}`\n"
) )

View File

@ -6,6 +6,7 @@ from freqtrade.exchange import (
timeframe_to_prev_date, timeframe_to_prev_date,
timeframe_to_seconds, timeframe_to_seconds,
) )
from freqtrade.persistence import Order, PairLocks, Trade
from freqtrade.strategy.informative_decorator import informative from freqtrade.strategy.informative_decorator import informative
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.parameters import ( from freqtrade.strategy.parameters import (
@ -20,3 +21,27 @@ from freqtrade.strategy.strategy_helper import (
stoploss_from_absolute, stoploss_from_absolute,
stoploss_from_open, stoploss_from_open,
) )
# Imports to be used for `from freqtrade.strategy import *`
__all__ = [
"IStrategy",
"Trade",
"Order",
"PairLocks",
"informative",
# Parameters
"BooleanParameter",
"CategoricalParameter",
"DecimalParameter",
"IntParameter",
"RealParameter",
# timeframe helpers
"timeframe_to_minutes",
"timeframe_to_next_date",
"timeframe_to_prev_date",
# Strategy helper functions
"merge_informative_pair",
"stoploss_from_absolute",
"stoploss_from_open",
]

View File

@ -6,6 +6,7 @@
"refresh_period": 1800 "refresh_period": 1800
}' %} }' %}
{ {
"$schema": "https://schema.freqtrade.io/schema.json",
"max_open_trades": {{ max_open_trades }}, "max_open_trades": {{ max_open_trades }},
"stake_currency": "{{ stake_currency }}", "stake_currency": "{{ stake_currency }}",
"stake_amount": {{ stake_amount }}, "stake_amount": {{ stake_amount }},

View File

@ -1,15 +1,34 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401 # flake8: noqa: F401
# isort: skip_file # isort: skip_file
# --- Do not remove these libs --- # --- Do not remove these imports ---
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame from pandas import DataFrame
from datetime import datetime from typing import Dict, Optional, Union, Tuple
from typing import Optional, Union
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, from freqtrade.strategy import (
IntParameter, IStrategy, merge_informative_pair) IStrategy,
Trade,
Order,
PairLocks,
informative, # @informative decorator
# Hyperopt Parameters
BooleanParameter,
CategoricalParameter,
DecimalParameter,
IntParameter,
RealParameter,
# timeframe helpers
timeframe_to_minutes,
timeframe_to_next_date,
timeframe_to_prev_date,
# Strategy helper functions
merge_informative_pair,
stoploss_from_absolute,
stoploss_from_open,
)
# -------------------------------- # --------------------------------
# Add your lib to import here # Add your lib to import here
@ -40,7 +59,7 @@ class {{ strategy }}(IStrategy):
INTERFACE_VERSION = 3 INTERFACE_VERSION = 3
# Optimal timeframe for the strategy. # Optimal timeframe for the strategy.
timeframe = '5m' timeframe = "5m"
# Can this strategy go short? # Can this strategy go short?
can_short: bool = False can_short: bool = False
@ -78,8 +97,8 @@ class {{ strategy }}(IStrategy):
buy_rsi = IntParameter(10, 40, default=30, space="buy") buy_rsi = IntParameter(10, 40, default=30, space="buy")
sell_rsi = IntParameter(60, 90, default=70, space="sell") sell_rsi = IntParameter(60, 90, default=70, space="sell")
{{ attributes | indent(4) }} {{- attributes | indent(4) }}
{{ plot_config | indent(4) }} {{- plot_config | indent(4) }}
def informative_pairs(self): def informative_pairs(self):
""" """
@ -105,7 +124,7 @@ class {{ strategy }}(IStrategy):
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies
""" """
{{ indicators | indent(8) }} {{- indicators | indent(8) }}
return dataframe return dataframe
@ -119,9 +138,9 @@ class {{ strategy }}(IStrategy):
dataframe.loc[ dataframe.loc[
( (
{{ buy_trend | indent(16) }} {{ buy_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0 (dataframe["volume"] > 0) # Make sure Volume is not 0
), ),
'enter_long'] = 1 "enter_long"] = 1
# Uncomment to use shorts (Only used in futures/margin mode. Check the documentation for more info) # Uncomment to use shorts (Only used in futures/margin mode. Check the documentation for more info)
""" """
dataframe.loc[ dataframe.loc[
@ -144,9 +163,9 @@ class {{ strategy }}(IStrategy):
dataframe.loc[ dataframe.loc[
( (
{{ sell_trend | indent(16) }} {{ sell_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0 (dataframe["volume"] > 0) # Make sure Volume is not 0
), ),
'exit_long'] = 1 "exit_long"] = 1
# Uncomment to use shorts (Only used in futures/margin mode. Check the documentation for more info) # Uncomment to use shorts (Only used in futures/margin mode. Check the documentation for more info)
""" """
dataframe.loc[ dataframe.loc[
@ -157,4 +176,4 @@ class {{ strategy }}(IStrategy):
'exit_short'] = 1 'exit_short'] = 1
""" """
return dataframe return dataframe
{{ additional_methods | indent(4) }} {{- additional_methods | indent(4) }}

View File

@ -1,24 +1,39 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401 # flake8: noqa: F401
# isort: skip_file # isort: skip_file
# --- Do not remove these libs --- # --- Do not remove these imports ---
import numpy as np # noqa import numpy as np
import pandas as pd # noqa import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame from pandas import DataFrame
from typing import Optional, Union from typing import Dict, Optional, Union, Tuple
from freqtrade.strategy import ( from freqtrade.strategy import (
IStrategy,
Trade,
Order,
PairLocks,
informative, # @informative decorator
# Hyperopt Parameters
BooleanParameter, BooleanParameter,
CategoricalParameter, CategoricalParameter,
DecimalParameter, DecimalParameter,
IStrategy,
IntParameter, IntParameter,
RealParameter,
# timeframe helpers
timeframe_to_minutes,
timeframe_to_next_date,
timeframe_to_prev_date,
# Strategy helper functions
merge_informative_pair,
stoploss_from_absolute,
stoploss_from_open,
) )
# -------------------------------- # --------------------------------
# Add your lib to import here # Add your lib to import here
import talib.abstract as ta import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib from technical import qtpylib
# This class is a sample. Feel free to customize it. # This class is a sample. Feel free to customize it.

View File

@ -29,19 +29,22 @@
"import os\n", "import os\n",
"from pathlib import Path\n", "from pathlib import Path\n",
"\n", "\n",
"\n",
"# Change directory\n", "# Change directory\n",
"# Modify this cell to insure that the output shows the correct path.\n", "# Modify this cell to insure that the output shows the correct path.\n",
"# Define all paths relative to the project root shown in the cell output\n", "# Define all paths relative to the project root shown in the cell output\n",
"project_root = \"somedir/freqtrade\"\n", "project_root = \"somedir/freqtrade\"\n",
"i=0\n", "i = 0\n",
"try:\n", "try:\n",
" os.chdir(project_root)\n", " os.chdir(project_root)\n",
" assert Path('LICENSE').is_file()\n", " if not Path(\"LICENSE\").is_file():\n",
"except:\n", " i = 0\n",
" while i<4 and (not Path('LICENSE').is_file()):\n", " while i < 4 and (not Path(\"LICENSE\").is_file()):\n",
" os.chdir(Path(Path.cwd(), '../'))\n", " os.chdir(Path(Path.cwd(), \"../\"))\n",
" i+=1\n", " i += 1\n",
" project_root = Path.cwd()\n", " project_root = Path.cwd()\n",
"except FileNotFoundError:\n",
" print(\"Please define the project root relative to the current directory\")\n",
"print(Path.cwd())" "print(Path.cwd())"
] ]
}, },
@ -60,6 +63,7 @@
"source": [ "source": [
"from freqtrade.configuration import Configuration\n", "from freqtrade.configuration import Configuration\n",
"\n", "\n",
"\n",
"# Customize these according to your needs.\n", "# Customize these according to your needs.\n",
"\n", "\n",
"# Initialize empty configuration object\n", "# Initialize empty configuration object\n",
@ -87,12 +91,14 @@
"from freqtrade.data.history import load_pair_history\n", "from freqtrade.data.history import load_pair_history\n",
"from freqtrade.enums import CandleType\n", "from freqtrade.enums import CandleType\n",
"\n", "\n",
"candles = load_pair_history(datadir=data_location,\n", "\n",
" timeframe=config[\"timeframe\"],\n", "candles = load_pair_history(\n",
" pair=pair,\n", " datadir=data_location,\n",
" data_format = \"json\", # Make sure to update this to your data\n", " timeframe=config[\"timeframe\"],\n",
" candle_type=CandleType.SPOT,\n", " pair=pair,\n",
" )\n", " data_format=\"json\", # Make sure to update this to your data\n",
" candle_type=CandleType.SPOT,\n",
")\n",
"\n", "\n",
"# Confirm success\n", "# Confirm success\n",
"print(f\"Loaded {len(candles)} rows of data for {pair} from {data_location}\")\n", "print(f\"Loaded {len(candles)} rows of data for {pair} from {data_location}\")\n",
@ -114,14 +120,16 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Load strategy using values set above\n", "# Load strategy using values set above\n",
"from freqtrade.resolvers import StrategyResolver\n",
"from freqtrade.data.dataprovider import DataProvider\n", "from freqtrade.data.dataprovider import DataProvider\n",
"from freqtrade.resolvers import StrategyResolver\n",
"\n",
"\n",
"strategy = StrategyResolver.load_strategy(config)\n", "strategy = StrategyResolver.load_strategy(config)\n",
"strategy.dp = DataProvider(config, None, None)\n", "strategy.dp = DataProvider(config, None, None)\n",
"strategy.ft_bot_start()\n", "strategy.ft_bot_start()\n",
"\n", "\n",
"# Generate buy/sell signals using strategy\n", "# Generate buy/sell signals using strategy\n",
"df = strategy.analyze_ticker(candles, {'pair': pair})\n", "df = strategy.analyze_ticker(candles, {\"pair\": pair})\n",
"df.tail()" "df.tail()"
] ]
}, },
@ -148,7 +156,7 @@
"source": [ "source": [
"# Report results\n", "# Report results\n",
"print(f\"Generated {df['enter_long'].sum()} entry signals\")\n", "print(f\"Generated {df['enter_long'].sum()} entry signals\")\n",
"data = df.set_index('date', drop=False)\n", "data = df.set_index(\"date\", drop=False)\n",
"data.tail()" "data.tail()"
] ]
}, },
@ -179,10 +187,13 @@
"source": [ "source": [
"from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats\n", "from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats\n",
"\n", "\n",
"\n",
"# if backtest_dir points to a directory, it'll automatically load the last backtest file.\n", "# if backtest_dir points to a directory, it'll automatically load the last backtest file.\n",
"backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n", "backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n",
"# backtest_dir can also point to a specific file\n", "# backtest_dir can also point to a specific file\n",
"# backtest_dir = config[\"user_data_dir\"] / \"backtest_results/backtest-result-2020-07-01_20-04-22.json\"" "# backtest_dir = (\n",
"# config[\"user_data_dir\"] / \"backtest_results/backtest-result-2020-07-01_20-04-22.json\"\n",
"# )"
] ]
}, },
{ {
@ -195,23 +206,24 @@
"# This contains all information used to generate the backtest result.\n", "# This contains all information used to generate the backtest result.\n",
"stats = load_backtest_stats(backtest_dir)\n", "stats = load_backtest_stats(backtest_dir)\n",
"\n", "\n",
"strategy = 'SampleStrategy'\n", "strategy = \"SampleStrategy\"\n",
"# All statistics are available per strategy, so if `--strategy-list` was used during backtest, this will be reflected here as well.\n", "# All statistics are available per strategy, so if `--strategy-list` was used during backtest,\n",
"# this will be reflected here as well.\n",
"# Example usages:\n", "# Example usages:\n",
"print(stats['strategy'][strategy]['results_per_pair'])\n", "print(stats[\"strategy\"][strategy][\"results_per_pair\"])\n",
"# Get pairlist used for this backtest\n", "# Get pairlist used for this backtest\n",
"print(stats['strategy'][strategy]['pairlist'])\n", "print(stats[\"strategy\"][strategy][\"pairlist\"])\n",
"# Get market change (average change of all pairs from start to end of the backtest period)\n", "# Get market change (average change of all pairs from start to end of the backtest period)\n",
"print(stats['strategy'][strategy]['market_change'])\n", "print(stats[\"strategy\"][strategy][\"market_change\"])\n",
"# Maximum drawdown ()\n", "# Maximum drawdown ()\n",
"print(stats['strategy'][strategy]['max_drawdown'])\n", "print(stats[\"strategy\"][strategy][\"max_drawdown\"])\n",
"# Maximum drawdown start and end\n", "# Maximum drawdown start and end\n",
"print(stats['strategy'][strategy]['drawdown_start'])\n", "print(stats[\"strategy\"][strategy][\"drawdown_start\"])\n",
"print(stats['strategy'][strategy]['drawdown_end'])\n", "print(stats[\"strategy\"][strategy][\"drawdown_end\"])\n",
"\n", "\n",
"\n", "\n",
"# Get strategy comparison (only relevant if multiple strategies were compared)\n", "# Get strategy comparison (only relevant if multiple strategies were compared)\n",
"print(stats['strategy_comparison'])\n" "print(stats[\"strategy_comparison\"])"
] ]
}, },
{ {
@ -242,23 +254,25 @@
"source": [ "source": [
"# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)\n", "# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)\n",
"\n", "\n",
"import pandas as pd\n",
"import plotly.express as px\n",
"\n",
"from freqtrade.configuration import Configuration\n", "from freqtrade.configuration import Configuration\n",
"from freqtrade.data.btanalysis import load_backtest_stats\n", "from freqtrade.data.btanalysis import load_backtest_stats\n",
"import plotly.express as px\n", "\n",
"import pandas as pd\n",
"\n", "\n",
"# strategy = 'SampleStrategy'\n", "# strategy = 'SampleStrategy'\n",
"# config = Configuration.from_files([\"user_data/config.json\"])\n", "# config = Configuration.from_files([\"user_data/config.json\"])\n",
"# backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n", "# backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n",
"\n", "\n",
"stats = load_backtest_stats(backtest_dir)\n", "stats = load_backtest_stats(backtest_dir)\n",
"strategy_stats = stats['strategy'][strategy]\n", "strategy_stats = stats[\"strategy\"][strategy]\n",
"\n", "\n",
"df = pd.DataFrame(columns=['dates','equity'], data=strategy_stats['daily_profit'])\n", "df = pd.DataFrame(columns=[\"dates\", \"equity\"], data=strategy_stats[\"daily_profit\"])\n",
"df['equity_daily'] = df['equity'].cumsum()\n", "df[\"equity_daily\"] = df[\"equity\"].cumsum()\n",
"\n", "\n",
"fig = px.line(df, x=\"dates\", y=\"equity_daily\")\n", "fig = px.line(df, x=\"dates\", y=\"equity_daily\")\n",
"fig.show()\n" "fig.show()"
] ]
}, },
{ {
@ -278,6 +292,7 @@
"source": [ "source": [
"from freqtrade.data.btanalysis import load_trades_from_db\n", "from freqtrade.data.btanalysis import load_trades_from_db\n",
"\n", "\n",
"\n",
"# Fetch trades from database\n", "# Fetch trades from database\n",
"trades = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", "trades = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n",
"\n", "\n",
@ -303,8 +318,9 @@
"source": [ "source": [
"from freqtrade.data.btanalysis import analyze_trade_parallelism\n", "from freqtrade.data.btanalysis import analyze_trade_parallelism\n",
"\n", "\n",
"\n",
"# Analyze the above\n", "# Analyze the above\n",
"parallel_trades = analyze_trade_parallelism(trades, '5m')\n", "parallel_trades = analyze_trade_parallelism(trades, \"5m\")\n",
"\n", "\n",
"parallel_trades.plot()" "parallel_trades.plot()"
] ]
@ -324,22 +340,23 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"from freqtrade.plot.plotting import generate_candlestick_graph\n", "from freqtrade.plot.plotting import generate_candlestick_graph\n",
"\n",
"\n",
"# Limit graph period to keep plotly quick and reactive\n", "# Limit graph period to keep plotly quick and reactive\n",
"\n", "\n",
"# Filter trades to one pair\n", "# Filter trades to one pair\n",
"trades_red = trades.loc[trades['pair'] == pair]\n", "trades_red = trades.loc[trades[\"pair\"] == pair]\n",
"\n", "\n",
"data_red = data['2019-06-01':'2019-06-10']\n", "data_red = data[\"2019-06-01\":\"2019-06-10\"]\n",
"# Generate candlestick graph\n", "# Generate candlestick graph\n",
"graph = generate_candlestick_graph(pair=pair,\n", "graph = generate_candlestick_graph(\n",
" data=data_red,\n", " pair=pair,\n",
" trades=trades_red,\n", " data=data_red,\n",
" indicators1=['sma20', 'ema50', 'ema55'],\n", " trades=trades_red,\n",
" indicators2=['rsi', 'macd', 'macdsignal', 'macdhist']\n", " indicators1=[\"sma20\", \"ema50\", \"ema55\"],\n",
" )\n", " indicators2=[\"rsi\", \"macd\", \"macdsignal\", \"macdhist\"],\n",
"\n", ")"
"\n"
] ]
}, },
{ {
@ -352,7 +369,7 @@
"# graph.show()\n", "# graph.show()\n",
"\n", "\n",
"# Render graph in a separate window\n", "# Render graph in a separate window\n",
"graph.show(renderer=\"browser\")\n" "graph.show(renderer=\"browser\")"
] ]
}, },
{ {
@ -370,11 +387,12 @@
"source": [ "source": [
"import plotly.figure_factory as ff\n", "import plotly.figure_factory as ff\n",
"\n", "\n",
"\n",
"hist_data = [trades.profit_ratio]\n", "hist_data = [trades.profit_ratio]\n",
"group_labels = ['profit_ratio'] # name of the dataset\n", "group_labels = [\"profit_ratio\"] # name of the dataset\n",
"\n", "\n",
"fig = ff.create_distplot(hist_data, group_labels, bin_size=0.01)\n", "fig = ff.create_distplot(hist_data, group_labels, bin_size=0.01)\n",
"fig.show()\n" "fig.show()"
] ]
}, },
{ {

View File

@ -1,3 +1,3 @@
(qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)) & # Signal: RSI crosses above buy_rsi (qtpylib.crossed_above(dataframe["rsi"], self.buy_rsi.value)) & # Signal: RSI crosses above buy_rsi
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle (dataframe["tema"] <= dataframe["bb_middleband"]) & # Guard: tema below BB middle
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising (dataframe["tema"] > dataframe["tema"].shift(1)) & # Guard: tema is raising

View File

@ -1 +1 @@
(qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)) & # Signal: RSI crosses above buy_rsi (qtpylib.crossed_above(dataframe["rsi"], self.buy_rsi.value)) & # Signal: RSI crosses above buy_rsi

View File

@ -3,24 +3,24 @@
# ------------------------------------ # ------------------------------------
# ADX # ADX
dataframe['adx'] = ta.ADX(dataframe) dataframe["adx"] = ta.ADX(dataframe)
# # Plus Directional Indicator / Movement # # Plus Directional Indicator / Movement
# dataframe['plus_dm'] = ta.PLUS_DM(dataframe) # dataframe["plus_dm"] = ta.PLUS_DM(dataframe)
# dataframe['plus_di'] = ta.PLUS_DI(dataframe) # dataframe["plus_di"] = ta.PLUS_DI(dataframe)
# # Minus Directional Indicator / Movement # # Minus Directional Indicator / Movement
# dataframe['minus_dm'] = ta.MINUS_DM(dataframe) # dataframe["minus_dm"] = ta.MINUS_DM(dataframe)
# dataframe['minus_di'] = ta.MINUS_DI(dataframe) # dataframe["minus_di"] = ta.MINUS_DI(dataframe)
# # Aroon, Aroon Oscillator # # Aroon, Aroon Oscillator
# aroon = ta.AROON(dataframe) # aroon = ta.AROON(dataframe)
# dataframe['aroonup'] = aroon['aroonup'] # dataframe["aroonup"] = aroon["aroonup"]
# dataframe['aroondown'] = aroon['aroondown'] # dataframe["aroondown"] = aroon["aroondown"]
# dataframe['aroonosc'] = ta.AROONOSC(dataframe) # dataframe["aroonosc"] = ta.AROONOSC(dataframe)
# # Awesome Oscillator # # Awesome Oscillator
# dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) # dataframe["ao"] = qtpylib.awesome_oscillator(dataframe)
# # Keltner Channel # # Keltner Channel
# keltner = qtpylib.keltner_channel(dataframe) # keltner = qtpylib.keltner_channel(dataframe)
@ -36,58 +36,58 @@ dataframe['adx'] = ta.ADX(dataframe)
# ) # )
# # Ultimate Oscillator # # Ultimate Oscillator
# dataframe['uo'] = ta.ULTOSC(dataframe) # dataframe["uo"] = ta.ULTOSC(dataframe)
# # Commodity Channel Index: values [Oversold:-100, Overbought:100] # # Commodity Channel Index: values [Oversold:-100, Overbought:100]
# dataframe['cci'] = ta.CCI(dataframe) # dataframe["cci"] = ta.CCI(dataframe)
# RSI # RSI
dataframe['rsi'] = ta.RSI(dataframe) dataframe["rsi"] = ta.RSI(dataframe)
# # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy) # # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy)
# rsi = 0.1 * (dataframe['rsi'] - 50) # rsi = 0.1 * (dataframe["rsi"] - 50)
# dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) # dataframe["fisher_rsi"] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1)
# # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy) # # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy)
# dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) # dataframe["fisher_rsi_norma"] = 50 * (dataframe["fisher_rsi"] + 1)
# # Stochastic Slow # # Stochastic Slow
# stoch = ta.STOCH(dataframe) # stoch = ta.STOCH(dataframe)
# dataframe['slowd'] = stoch['slowd'] # dataframe["slowd"] = stoch["slowd"]
# dataframe['slowk'] = stoch['slowk'] # dataframe["slowk"] = stoch["slowk"]
# Stochastic Fast # Stochastic Fast
stoch_fast = ta.STOCHF(dataframe) stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd'] dataframe["fastd"] = stoch_fast["fastd"]
dataframe['fastk'] = stoch_fast['fastk'] dataframe["fastk"] = stoch_fast["fastk"]
# # Stochastic RSI # # Stochastic RSI
# Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. # Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this.
# STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. # STOCHRSI is NOT aligned with tradingview, which may result in non-expected results.
# stoch_rsi = ta.STOCHRSI(dataframe) # stoch_rsi = ta.STOCHRSI(dataframe)
# dataframe['fastd_rsi'] = stoch_rsi['fastd'] # dataframe["fastd_rsi"] = stoch_rsi["fastd"]
# dataframe['fastk_rsi'] = stoch_rsi['fastk'] # dataframe["fastk_rsi"] = stoch_rsi["fastk"]
# MACD # MACD
macd = ta.MACD(dataframe) macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd'] dataframe["macd"] = macd["macd"]
dataframe['macdsignal'] = macd['macdsignal'] dataframe["macdsignal"] = macd["macdsignal"]
dataframe['macdhist'] = macd['macdhist'] dataframe["macdhist"] = macd["macdhist"]
# MFI # MFI
dataframe['mfi'] = ta.MFI(dataframe) dataframe["mfi"] = ta.MFI(dataframe)
# # ROC # # ROC
# dataframe['roc'] = ta.ROC(dataframe) # dataframe["roc"] = ta.ROC(dataframe)
# Overlap Studies # Overlap Studies
# ------------------------------------ # ------------------------------------
# Bollinger Bands # Bollinger Bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower'] dataframe["bb_lowerband"] = bollinger["lower"]
dataframe['bb_middleband'] = bollinger['mid'] dataframe["bb_middleband"] = bollinger["mid"]
dataframe['bb_upperband'] = bollinger['upper'] dataframe["bb_upperband"] = bollinger["upper"]
dataframe["bb_percent"] = ( dataframe["bb_percent"] = (
(dataframe["close"] - dataframe["bb_lowerband"]) / (dataframe["close"] - dataframe["bb_lowerband"]) /
(dataframe["bb_upperband"] - dataframe["bb_lowerband"]) (dataframe["bb_upperband"] - dataframe["bb_lowerband"])
@ -112,95 +112,95 @@ dataframe["bb_width"] = (
# ) # )
# # EMA - Exponential Moving Average # # EMA - Exponential Moving Average
# dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) # dataframe["ema3"] = ta.EMA(dataframe, timeperiod=3)
# dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) # dataframe["ema5"] = ta.EMA(dataframe, timeperiod=5)
# dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) # dataframe["ema10"] = ta.EMA(dataframe, timeperiod=10)
# dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21) # dataframe["ema21"] = ta.EMA(dataframe, timeperiod=21)
# dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) # dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
# dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) # dataframe["ema100"] = ta.EMA(dataframe, timeperiod=100)
# # SMA - Simple Moving Average # # SMA - Simple Moving Average
# dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3) # dataframe["sma3"] = ta.SMA(dataframe, timeperiod=3)
# dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5) # dataframe["sma5"] = ta.SMA(dataframe, timeperiod=5)
# dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10) # dataframe["sma10"] = ta.SMA(dataframe, timeperiod=10)
# dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21) # dataframe["sma21"] = ta.SMA(dataframe, timeperiod=21)
# dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50) # dataframe["sma50"] = ta.SMA(dataframe, timeperiod=50)
# dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100) # dataframe["sma100"] = ta.SMA(dataframe, timeperiod=100)
# Parabolic SAR # Parabolic SAR
dataframe['sar'] = ta.SAR(dataframe) dataframe["sar"] = ta.SAR(dataframe)
# TEMA - Triple Exponential Moving Average # TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator # Cycle Indicator
# ------------------------------------ # ------------------------------------
# Hilbert Transform Indicator - SineWave # Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe) hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine'] dataframe["htsine"] = hilbert["sine"]
dataframe['htleadsine'] = hilbert['leadsine'] dataframe["htleadsine"] = hilbert["leadsine"]
# Pattern Recognition - Bullish candlestick patterns # Pattern Recognition - Bullish candlestick patterns
# ------------------------------------ # ------------------------------------
# # Hammer: values [0, 100] # # Hammer: values [0, 100]
# dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) # dataframe["CDLHAMMER"] = ta.CDLHAMMER(dataframe)
# # Inverted Hammer: values [0, 100] # # Inverted Hammer: values [0, 100]
# dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) # dataframe["CDLINVERTEDHAMMER"] = ta.CDLINVERTEDHAMMER(dataframe)
# # Dragonfly Doji: values [0, 100] # # Dragonfly Doji: values [0, 100]
# dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) # dataframe["CDLDRAGONFLYDOJI"] = ta.CDLDRAGONFLYDOJI(dataframe)
# # Piercing Line: values [0, 100] # # Piercing Line: values [0, 100]
# dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] # dataframe["CDLPIERCING"] = ta.CDLPIERCING(dataframe) # values [0, 100]
# # Morningstar: values [0, 100] # # Morningstar: values [0, 100]
# dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] # dataframe["CDLMORNINGSTAR"] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# # Three White Soldiers: values [0, 100] # # Three White Soldiers: values [0, 100]
# dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] # dataframe["CDL3WHITESOLDIERS"] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
# Pattern Recognition - Bearish candlestick patterns # Pattern Recognition - Bearish candlestick patterns
# ------------------------------------ # ------------------------------------
# # Hanging Man: values [0, 100] # # Hanging Man: values [0, 100]
# dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) # dataframe["CDLHANGINGMAN"] = ta.CDLHANGINGMAN(dataframe)
# # Shooting Star: values [0, 100] # # Shooting Star: values [0, 100]
# dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) # dataframe["CDLSHOOTINGSTAR"] = ta.CDLSHOOTINGSTAR(dataframe)
# # Gravestone Doji: values [0, 100] # # Gravestone Doji: values [0, 100]
# dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) # dataframe["CDLGRAVESTONEDOJI"] = ta.CDLGRAVESTONEDOJI(dataframe)
# # Dark Cloud Cover: values [0, 100] # # Dark Cloud Cover: values [0, 100]
# dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) # dataframe["CDLDARKCLOUDCOVER"] = ta.CDLDARKCLOUDCOVER(dataframe)
# # Evening Doji Star: values [0, 100] # # Evening Doji Star: values [0, 100]
# dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) # dataframe["CDLEVENINGDOJISTAR"] = ta.CDLEVENINGDOJISTAR(dataframe)
# # Evening Star: values [0, 100] # # Evening Star: values [0, 100]
# dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) # dataframe["CDLEVENINGSTAR"] = ta.CDLEVENINGSTAR(dataframe)
# Pattern Recognition - Bullish/Bearish candlestick patterns # Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------ # ------------------------------------
# # Three Line Strike: values [0, -100, 100] # # Three Line Strike: values [0, -100, 100]
# dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) # dataframe["CDL3LINESTRIKE"] = ta.CDL3LINESTRIKE(dataframe)
# # Spinning Top: values [0, -100, 100] # # Spinning Top: values [0, -100, 100]
# dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] # dataframe["CDLSPINNINGTOP"] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# # Engulfing: values [0, -100, 100] # # Engulfing: values [0, -100, 100]
# dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] # dataframe["CDLENGULFING"] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# # Harami: values [0, -100, 100] # # Harami: values [0, -100, 100]
# dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] # dataframe["CDLHARAMI"] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# # Three Outside Up/Down: values [0, -100, 100] # # Three Outside Up/Down: values [0, -100, 100]
# dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] # dataframe["CDL3OUTSIDE"] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# # Three Inside Up/Down: values [0, -100, 100] # # Three Inside Up/Down: values [0, -100, 100]
# dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] # dataframe["CDL3INSIDE"] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
# # Chart type # # Chart type
# # ------------------------------------ # # ------------------------------------
# # Heikin Ashi Strategy # # Heikin Ashi Strategy
# heikinashi = qtpylib.heikinashi(dataframe) # heikinashi = qtpylib.heikinashi(dataframe)
# dataframe['ha_open'] = heikinashi['open'] # dataframe["ha_open"] = heikinashi["open"]
# dataframe['ha_close'] = heikinashi['close'] # dataframe["ha_close"] = heikinashi["close"]
# dataframe['ha_high'] = heikinashi['high'] # dataframe["ha_high"] = heikinashi["high"]
# dataframe['ha_low'] = heikinashi['low'] # dataframe["ha_low"] = heikinashi["low"]
# Retrieve best bid and best ask from the orderbook # Retrieve best bid and best ask from the orderbook
# ------------------------------------ # ------------------------------------
""" """
# first check if dataprovider is available # first check if dataprovider is available
if self.dp: if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'): 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]
""" """

View File

@ -3,15 +3,15 @@
# ------------------------------------ # ------------------------------------
# RSI # RSI
dataframe['rsi'] = ta.RSI(dataframe) dataframe["rsi"] = ta.RSI(dataframe)
# Retrieve best bid and best ask from the orderbook # Retrieve best bid and best ask from the orderbook
# ------------------------------------ # ------------------------------------
""" """
# first check if dataprovider is available # first check if dataprovider is available
if self.dp: if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'): 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]
""" """

View File

@ -3,18 +3,18 @@
def plot_config(self): def plot_config(self):
return { return {
# Main plot indicators (Moving averages, ...) # Main plot indicators (Moving averages, ...)
'main_plot': { "main_plot": {
'tema': {}, "tema": {},
'sar': {'color': 'white'}, "sar": {"color": "white"},
}, },
'subplots': { "subplots": {
# Subplots - each dict defines one additional plot # Subplots - each dict defines one additional plot
"MACD": { "MACD": {
'macd': {'color': 'blue'}, "macd": {"color": "blue"},
'macdsignal': {'color': 'orange'}, "macdsignal": {"color": "orange"},
}, },
"RSI": { "RSI": {
'rsi': {'color': 'red'}, "rsi": {"color": "red"},
} }
} }
} }

View File

@ -1,3 +1,3 @@
(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) & # Signal: RSI crosses above sell_rsi (qtpylib.crossed_above(dataframe["rsi"], self.sell_rsi.value)) & # Signal: RSI crosses above sell_rsi
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle (dataframe["tema"] > dataframe["bb_middleband"]) & # Guard: tema above BB middle
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling (dataframe["tema"] < dataframe["tema"].shift(1)) & # Guard: tema is falling

View File

@ -1 +1 @@
(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) & # Signal: RSI crosses above sell_rsi (qtpylib.crossed_above(dataframe["rsi"], self.sell_rsi.value)) & # Signal: RSI crosses above sell_rsi

View File

@ -1,13 +1,13 @@
# Optional order type mapping. # Optional order type mapping.
order_types = { order_types = {
'entry': 'limit', "entry": "limit",
'exit': 'limit', "exit": "limit",
'stoploss': 'market', "stoploss": "market",
'stoploss_on_exchange': False "stoploss_on_exchange": False
} }
# Optional order time in force. # Optional order time in force.
order_time_in_force = { order_time_in_force = {
'entry': 'GTC', "entry": "GTC",
'exit': 'GTC' "exit": "GTC"
} }

View File

@ -13,9 +13,9 @@ def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
""" """
pass pass
def custom_entry_price(self, pair: str, trade: Optional['Trade'], def custom_entry_price(self, pair: str, trade: Optional[Trade],
current_time: 'datetime', proposed_rate: float, current_time: datetime, proposed_rate: float,
entry_tag: 'Optional[str]', side: str, **kwargs) -> float: entry_tag: Optional[str], side: str, **kwargs) -> float:
""" """
Custom entry price logic, returning the new entry price. Custom entry price logic, returning the new entry price.
@ -33,7 +33,7 @@ def custom_entry_price(self, pair: str, trade: Optional['Trade'],
""" """
return proposed_rate return proposed_rate
def adjust_entry_price(self, trade: 'Trade', order: 'Optional[Order]', pair: str, def adjust_entry_price(self, trade: Trade, order: Optional[Order], pair: str,
current_time: datetime, proposed_rate: float, current_order_rate: float, current_time: datetime, proposed_rate: float, current_order_rate: float,
entry_tag: Optional[str], side: str, **kwargs) -> float: entry_tag: Optional[str], side: str, **kwargs) -> float:
""" """
@ -61,8 +61,8 @@ def adjust_entry_price(self, trade: 'Trade', order: 'Optional[Order]', pair: str
""" """
return current_order_rate return current_order_rate
def custom_exit_price(self, pair: str, trade: 'Trade', def custom_exit_price(self, pair: str, trade: Trade,
current_time: 'datetime', proposed_rate: float, current_time: datetime, proposed_rate: float,
current_profit: float, exit_tag: Optional[str], **kwargs) -> float: current_profit: float, exit_tag: Optional[str], **kwargs) -> float:
""" """
Custom exit price logic, returning the new exit price. Custom exit price logic, returning the new exit price.
@ -104,7 +104,7 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, after_fill: bool, **kwargs) -> float: current_profit: float, after_fill: bool, **kwargs) -> float:
""" """
Custom stoploss logic, returning the new distance relative to current_rate (as ratio). Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
@ -126,8 +126,8 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', c
:return float: New stoploss value, relative to the current_rate :return float: New stoploss value, relative to the current_rate
""" """
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]': current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
""" """
Custom exit signal logic indicating that specified position should be sold. Returning a Custom exit signal logic indicating that specified position should be sold. Returning a
string or True from this method is equal to setting sell signal on a candle at specified string or True from this method is equal to setting sell signal on a candle at specified
@ -177,9 +177,9 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
""" """
return True return True
def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, exit_reason: str, rate: float, time_in_force: str, exit_reason: str,
current_time: 'datetime', **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
Called right before placing a regular exit order. Called right before placing a regular exit order.
Timing for this function is critical, so avoid doing heavy computations or Timing for this function is critical, so avoid doing heavy computations or
@ -206,7 +206,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
""" """
return True return True
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order', def check_entry_timeout(self, pair: str, trade: Trade, order: Order,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
Check entry timeout function callback. Check entry timeout function callback.
@ -228,7 +228,7 @@ def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
""" """
return False return False
def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order', def check_exit_timeout(self, pair: str, trade: Trade, order: Order,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
Check exit timeout function callback. Check exit timeout function callback.
@ -250,7 +250,7 @@ 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], max_stake: float, min_stake: Optional[float], max_stake: float,
current_entry_rate: float, current_exit_rate: float, current_entry_rate: float, current_exit_rate: float,
@ -302,7 +302,7 @@ def leverage(self, pair: str, current_time: datetime, current_rate: float,
return 1.0 return 1.0
def order_filled(self, pair: str, trade: 'Trade', order: 'Order', def order_filled(self, pair: str, trade: Trade, order: Order,
current_time: datetime, **kwargs) -> None: current_time: datetime, **kwargs) -> None:
""" """
Called right after an order fills. Called right after an order fills.

View File

@ -1,5 +1,5 @@
# Used for list-exchanges # Used for list-exchanges
from typing import List from typing import List, Optional
from typing_extensions import TypedDict from typing_extensions import TypedDict
@ -11,8 +11,11 @@ class TradeModeType(TypedDict):
class ValidExchangesType(TypedDict): class ValidExchangesType(TypedDict):
name: str name: str
classname: str
valid: bool valid: bool
supported: bool supported: bool
comment: str comment: str
dex: bool dex: bool
is_alias: bool
alias_for: Optional[str]
trade_modes: List[TradeModeType] trade_modes: List[TradeModeType]

View File

@ -1,14 +1,18 @@
from typing import Union from typing import Callable, List, Union
from rich.console import ConsoleRenderable, Group, RichCast from rich.console import ConsoleRenderable, Group, RichCast
from rich.progress import Progress from rich.progress import Progress
class CustomProgress(Progress): class CustomProgress(Progress):
def __init__(self, *args, cust_objs=[], **kwargs) -> None: def __init__(self, *args, cust_objs=[], cust_callables: List[Callable] = [], **kwargs) -> None:
self._cust_objs = cust_objs self._cust_objs = cust_objs
self._cust_callables = cust_callables
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def get_renderable(self) -> Union[ConsoleRenderable, RichCast, str]: def get_renderable(self) -> Union[ConsoleRenderable, RichCast, str]:
renderable = Group(*self._cust_objs, *self.get_renderables()) objs = [obj for obj in self._cust_objs]
for cust_call in self._cust_callables:
objs.append(cust_call())
renderable = Group(*objs, *self.get_renderables())
return renderable return renderable

View File

@ -37,9 +37,11 @@ def print_rich_table(
row_to_add: List[Union[str, Text]] = [r if isinstance(r, Text) else str(r) for r in row] row_to_add: List[Union[str, Text]] = [r if isinstance(r, Text) else str(r) for r in row]
table.add_row(*row_to_add) table.add_row(*row_to_add)
console = Console( width = None
width=200 if "pytest" in sys.modules else None, if any(module in ["pytest", "ipykernel"] for module in sys.modules):
) width = 200
console = Console(width=width)
console.print(table) console.print(table)
@ -71,7 +73,9 @@ def print_df_rich_table(
row = [_format_value(x, floatfmt=".3f") for x in value_list] row = [_format_value(x, floatfmt=".3f") for x in value_list]
table.add_row(*row) table.add_row(*row)
console = Console( width = None
width=200 if "pytest" in sys.modules else None, if any(module in ["pytest", "ipykernel"] for module in sys.modules):
) width = 200
console = Console(width=width)
console.print(table) console.print(table)

View File

@ -29,7 +29,7 @@ class Wallet(NamedTuple):
class PositionWallet(NamedTuple): class PositionWallet(NamedTuple):
symbol: str symbol: str
position: float = 0 position: float = 0
leverage: float = 0 leverage: Optional[float] = 0 # Don't use this - it's not guaranteed to be set
collateral: float = 0 collateral: float = 0
side: str = "long" side: str = "long"
@ -66,6 +66,17 @@ class Wallets:
else: else:
return 0 return 0
def get_owned(self, pair: str, base_currency: str) -> float:
"""
Get currently owned value.
Designed to work across both spot and futures.
"""
if self._config.get("trading_mode", "spot") != TradingMode.FUTURES:
return self.get_total(base_currency) or 0
if pos := self._positions.get(pair):
return pos.position
return 0
def _update_dry(self) -> None: def _update_dry(self) -> None:
""" """
Update from database in dry-run mode Update from database in dry-run mode
@ -82,7 +93,7 @@ class Wallets:
tot_profit = Trade.get_total_closed_profit() tot_profit = Trade.get_total_closed_profit()
else: else:
# Backtest mode # Backtest mode
tot_profit = LocalTrade.total_profit tot_profit = LocalTrade.bt_total_profit
tot_profit += sum(trade.realized_profit for trade in open_trades) tot_profit += sum(trade.realized_profit for trade in open_trades)
tot_in_trades = sum(trade.stake_amount for trade in open_trades) tot_in_trades = sum(trade.stake_amount for trade in open_trades)
used_stake = 0.0 used_stake = 0.0
@ -128,9 +139,9 @@ class Wallets:
if isinstance(balances[currency], dict): if isinstance(balances[currency], dict):
self._wallets[currency] = Wallet( self._wallets[currency] = Wallet(
currency, currency,
balances[currency].get("free"), balances[currency].get("free", 0),
balances[currency].get("used"), balances[currency].get("used", 0),
balances[currency].get("total"), balances[currency].get("total", 0),
) )
# Remove currencies no longer in get_balances output # Remove currencies no longer in get_balances output
for currency in deepcopy(self._wallets): for currency in deepcopy(self._wallets):
@ -138,7 +149,7 @@ class Wallets:
del self._wallets[currency] del self._wallets[currency]
positions = self._exchange.fetch_positions() positions = self._exchange.fetch_positions()
self._positions = {} _parsed_positions = {}
for position in positions: for position in positions:
symbol = position["symbol"] symbol = position["symbol"]
if position["side"] is None or position["collateral"] == 0.0: if position["side"] is None or position["collateral"] == 0.0:
@ -146,14 +157,15 @@ class Wallets:
continue continue
size = self._exchange._contracts_to_amount(symbol, position["contracts"]) size = self._exchange._contracts_to_amount(symbol, position["contracts"])
collateral = safe_value_fallback(position, "collateral", "initialMargin", 0.0) collateral = safe_value_fallback(position, "collateral", "initialMargin", 0.0)
leverage = position["leverage"] leverage = position.get("leverage")
self._positions[symbol] = PositionWallet( _parsed_positions[symbol] = PositionWallet(
symbol, symbol,
position=size, position=size,
leverage=leverage, leverage=leverage,
collateral=collateral, collateral=collateral,
side=position["side"], side=position["side"],
) )
self._positions = _parsed_positions
def update(self, require_update: bool = True) -> None: def update(self, require_update: bool = True) -> None:
""" """

View File

@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2024.7.1" __version__ = "2024.8"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path

View File

@ -1,3 +1,3 @@
# Requirements for freqtrade client library # Requirements for freqtrade client library
requests==2.32.3 requests==2.32.3
python-rapidjson==1.18 python-rapidjson==1.20

View File

@ -112,3 +112,12 @@ markdown_extensions:
custom_checkbox: true custom_checkbox: true
- pymdownx.tilde - pymdownx.tilde
- mdx_truly_sane_lists - mdx_truly_sane_lists
extra:
version:
provider: mike
alias: true
plugins:
- search:
enabled: true
- mike:
deploy_prefix: 'en'

View File

@ -86,6 +86,7 @@ log_format = "%(asctime)s %(levelname)s %(message)s"
log_date_format = "%Y-%m-%d %H:%M:%S" log_date_format = "%Y-%m-%d %H:%M:%S"
asyncio_mode = "auto" asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
addopts = "--dist loadscope" addopts = "--dist loadscope"
[tool.mypy] [tool.mypy]

View File

@ -7,11 +7,11 @@
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
coveralls==4.0.1 coveralls==4.0.1
ruff==0.5.4 ruff==0.6.2
mypy==1.11.0 mypy==1.11.2
pre-commit==3.7.1 pre-commit==3.8.0
pytest==8.3.1 pytest==8.3.2
pytest-asyncio==0.23.8 pytest-asyncio==0.24.0
pytest-cov==5.0.0 pytest-cov==5.0.0
pytest-mock==3.14.0 pytest-mock==3.14.0
pytest-random-order==1.1.1 pytest-random-order==1.1.1
@ -19,14 +19,14 @@ pytest-timeout==2.3.1
pytest-xdist==3.6.1 pytest-xdist==3.6.1
isort==5.13.2 isort==5.13.2
# For datetime mocking # For datetime mocking
time-machine==2.14.2 time-machine==2.15.0
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==7.16.4 nbconvert==7.16.4
# mypy types # mypy types
types-cachetools==5.4.0.20240717 types-cachetools==5.5.0.20240820
types-filelock==3.2.7 types-filelock==3.2.7
types-requests==2.32.0.20240712 types-requests==2.32.0.20240712
types-tabulate==0.9.0.20240106 types-tabulate==0.9.0.20240106
types-python-dateutil==2.9.0.20240316 types-python-dateutil==2.9.0.20240821

View File

@ -2,10 +2,10 @@
-r requirements-freqai.txt -r requirements-freqai.txt
# Required for freqai-rl # Required for freqai-rl
torch==2.3.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
torch==2.2.2; sys_platform == 'darwin' and platform_machine == 'x86_64' torch==2.2.2; sys_platform == 'darwin' and platform_machine == 'x86_64'
torch==2.4.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==0.29.1 gymnasium==0.29.1
stable_baselines3==2.3.2 stable_baselines3==2.3.2
sb3_contrib>=2.2.1 sb3_contrib>=2.2.1
# Progress bar for stable-baselines3 and sb3-contrib # Progress bar for stable-baselines3 and sb3-contrib
tqdm==4.66.4 tqdm==4.66.5

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