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
updates:
- package-ecosystem: docker
directory: "/"
directories:
- "/"
- "/docker"
schedule:
interval: daily
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
open-pull-requests-limit: 10
- package-ecosystem: pip

View File

@ -55,7 +55,7 @@ jobs:
- name: Installation - *nix
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 TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
@ -197,7 +197,7 @@ jobs:
- name: Installation (python)
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 TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
@ -384,7 +384,6 @@ jobs:
- name: Documentation build
run: |
pip install -r docs/requirements-docs.txt
pip install mkdocs
mkdocs build
- name: Discord notification
@ -427,7 +426,7 @@ jobs:
- name: Installation - *nix
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 TA_LIBRARY_PATH=${HOME}/dependencies/lib
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_kraken.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
repos:
- repo: https://github.com/pycqa/flake8
rev: "7.1.0"
rev: "7.1.1"
hooks:
- id: flake8
additional_dependencies: [Flake8-pyproject]
# stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.11.0"
rev: "v1.11.2"
hooks:
- id: mypy
exclude: build_helpers
additional_dependencies:
- types-cachetools==5.4.0.20240717
- types-cachetools==5.5.0.20240820
- types-filelock==3.2.7
- types-requests==2.32.0.20240712
- types-tabulate==0.9.0.20240106
- types-python-dateutil==2.9.0.20240316
- SQLAlchemy==2.0.31
- types-python-dateutil==2.9.0.20240821
- SQLAlchemy==2.0.32
# stages: [push]
- repo: https://github.com/pycqa/isort
@ -31,7 +31,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.5.4'
rev: 'v0.6.2'
hooks:
- 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
ENV LANG C.UTF-8
@ -25,7 +25,7 @@ FROM base as python-deps
RUN apt-get update \
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
&& apt-get clean \
&& pip install --upgrade "pip<=24.0" wheel
&& pip install --upgrade pip wheel
# Install TA-lib
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]
{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
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.
create-userdir Create user-data directory.
new-config Create new config
show-config Show resolved config
new-strategy Create new strategy
download-data Download backtesting data.
convert-data Convert candle (OHLCV) 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.
backtesting Backtesting module.
backtesting-show Show past Backtest results
backtesting-analysis
Backtest Analysis module.
edge Edge module.
hyperopt Hyperopt module.
hyperopt-list List Hyperopt results
hyperopt-show Show details of Hyperopt results
list-exchanges Print available exchanges.
list-hyperopts Print available hyperopt classes.
list-markets Print markets on exchange.
list-pairs Print pairs on exchange.
list-strategies Print available strategies.
list-freqaimodels Print available freqAI models.
list-timeframes Print available timeframes for the exchange.
show-trades Show trades.
test-pairlist Test your pairlist configuration.
convert-db Migrate database to different system
install-ui Install FreqUI
plot-dataframe Plot candles with indicators.
plot-profit Generate plot showing profits.
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
-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
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}')"

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"max_open_trades": 5,
"stake_currency": "EUR",
"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
ENV LANG C.UTF-8
@ -17,7 +17,7 @@ RUN mkdir /freqtrade \
&& chown ftuser:ftuser /freqtrade \
# Allow sudoers
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \
&& pip install --upgrade "pip<=24.0"
&& pip install --upgrade pip
WORKDIR /freqtrade

View File

@ -30,11 +30,17 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
"""
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
config: Config, processed: Dict[str, DataFrame],
backtest_stats: Dict[str, Any],
*args, **kwargs) -> float:
def hyperopt_loss_function(
*,
results: DataFrame,
trade_count: int,
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
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:
- 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
- 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
- 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%)

View File

@ -12,41 +12,50 @@ This page explains the different parameters of the bot and how to run it.
```
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
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.
create-userdir Create user-data directory.
new-config Create new config
show-config Show resolved config
new-strategy Create new strategy
download-data Download backtesting data.
convert-data Convert candle (OHLCV) 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.
backtesting Backtesting module.
backtesting-show Show past Backtest results
backtesting-analysis
Backtest Analysis module.
edge Edge module.
hyperopt Hyperopt module.
hyperopt-list List Hyperopt results
hyperopt-show Show details of Hyperopt results
list-exchanges Print available exchanges.
list-hyperopts Print available hyperopt classes.
list-markets Print markets on exchange.
list-pairs Print pairs on exchange.
list-strategies Print available strategies.
list-freqaimodels Print available freqAI models.
list-timeframes Print available timeframes for the exchange.
show-trades Show trades.
test-pairlist Test your pairlist configuration.
convert-db Migrate database to different system
install-ui Install FreqUI
plot-dataframe Plot candles with indicators.
plot-profit Generate plot showing profits.
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
-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).
## 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
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]
[--userdir PATH] [--exchange EXCHANGE]
[--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}]
[--show-timerange]
@ -433,6 +434,10 @@ options:
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
Storage format for downloaded candle (OHLCV) data.
(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 ...]
Limit command to these pairs. Pairs are space-
separated.
@ -465,13 +470,29 @@ Common arguments:
```bash
> freqtrade list-data --userdir ~/.freqtrade/user_data/
Found 33 pair / timeframe combinations.
pairs timeframe
---------- -----------------------------------------
ADA/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
ADA/ETH 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
ETH/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
Found 33 pair / timeframe combinations.
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━┓
┃ Pair ┃ Timeframe ┃ Type ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━┩
│ ADA/BTC │ 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d │ spot │
│ ADA/ETH │ 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d │ spot │
│ 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

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.
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.
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.
@ -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)
* [`VolumePairList`](#volume-pair-list)
* [`PercentChangePairList`](#percent-change-pair-list)
* [`ProducerPairList`](#producerpairlist)
* [`RemotePairList`](#remotepairlist)
* [`MarketCapPairList`](#marketcappairlist)
@ -152,6 +153,89 @@ More sophisticated approach can be used, by using `lookback_timeframe` for candl
!!! Note
`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
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` | 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
| `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"
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
`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.
@ -97,7 +98,7 @@ def protections(self):
#### 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.
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.
@ -120,7 +121,7 @@ def protections(self):
#### 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".

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-material==9.5.29
mkdocs-material==9.5.33
mdx_truly_sane_lists==1.3
pymdown-extensions==10.8.1
pymdown-extensions==10.9
jinja2==3.1.4
mike==2.1.3

View File

@ -24,6 +24,8 @@ Currently available callbacks:
!!! Tip "Callback calling sequence"
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
--8<-- "includes/strategy-imports.md"
## Bot start
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.
: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.*
# 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.
``` python
# Default imports
import requests
class AwesomeStrategy(IStrategy):
@ -71,10 +74,10 @@ class AwesomeStrategy(IStrategy):
: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.
"""
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.*
# 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.
```python
# Default imports
class AwesomeStrategy(IStrategy):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: 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)
current_candle = dataframe.iloc[-1].squeeze()
if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']:
if self.config['stake_amount'] == 'unlimited':
if current_candle["fastk_rsi_1h"] > current_candle["fastd_rsi_1h"]:
if self.config["stake_amount"] == "unlimited":
# Use entire available wallet during favorable conditions when in compounding mode.
return max_stake
else:
# 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.
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:
``` python
# Default imports
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):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
# Above 20% profit, sell when rsi < 80
if current_profit > 0.2:
if last_candle['rsi'] < 80:
return 'rsi_below_80'
if last_candle["rsi"] < 80:
return "rsi_below_80"
# Between 2% and 10%, sell if EMA-long above EMA-short
if 0.02 < current_profit < 0.1:
if last_candle['emalong'] > last_candle['emashort']:
return 'ema_long_below_80'
if last_candle["emalong"] > last_candle["emashort"]:
return "ema_long_below_80"
# 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:
return 'unclog'
return "unclog"
```
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.
`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)).
!!! 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:
``` python
# additional imports required
from datetime import datetime
from freqtrade.persistence import Trade
# Default imports
class AwesomeStrategy(IStrategy):
@ -206,7 +210,7 @@ class AwesomeStrategy(IStrategy):
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,
**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.
``` python
from datetime import datetime, timedelta
from freqtrade.persistence import Trade
# Default imports
class AwesomeStrategy(IStrategy):
@ -245,7 +248,7 @@ class AwesomeStrategy(IStrategy):
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,
**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)).
``` python
from datetime import datetime, timedelta
from freqtrade.persistence import Trade
# Default imports
class AwesomeStrategy(IStrategy):
@ -272,7 +274,7 @@ class AwesomeStrategy(IStrategy):
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,
**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.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
# Default imports
class AwesomeStrategy(IStrategy):
@ -302,13 +303,13 @@ class AwesomeStrategy(IStrategy):
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,
**kwargs) -> Optional[float]:
if pair in ('ETH/BTC', 'XRP/BTC'):
if pair in ("ETH/BTC", "XRP/BTC"):
return -0.10
elif pair in ('LTC/BTC'):
elif pair in ("LTC/BTC"):
return -0.05
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.
``` python
from datetime import datetime, timedelta
from freqtrade.persistence import Trade
# Default imports
class AwesomeStrategy(IStrategy):
@ -329,7 +329,7 @@ class AwesomeStrategy(IStrategy):
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,
**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.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import stoploss_from_open
# Default imports
class AwesomeStrategy(IStrategy):
@ -363,7 +361,7 @@ class AwesomeStrategy(IStrategy):
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,
**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.
``` python
# Default imports
class AwesomeStrategy(IStrategy):
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# <...>
dataframe['sar'] = ta.SAR(dataframe)
dataframe["sar"] = ta.SAR(dataframe)
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,
**kwargs) -> Optional[float]:
@ -400,7 +400,7 @@ class AwesomeStrategy(IStrategy):
last_candle = dataframe.iloc[-1].squeeze()
# 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
if stoploss_price < current_rate:
@ -429,10 +429,7 @@ Stoploss values returned from `custom_stoploss()` must specify a percentage rela
``` python
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, stoploss_from_open
# Default imports
class AwesomeStrategy(IStrategy):
@ -440,7 +437,7 @@ Stoploss values returned from `custom_stoploss()` must specify a percentage rela
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,
**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"
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.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, stoploss_from_absolute, timeframe_to_prev_date
# Default imports
class AwesomeStrategy(IStrategy):
use_custom_stoploss = True
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
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,
**kwargs) -> Optional[float]:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
candle = dataframe.iloc[-1].squeeze()
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,
is_short=trade.is_short,
leverage=trade.leverage)
```
---
## 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
``` python
from datetime import datetime, timedelta, timezone
from freqtrade.persistence import Trade
# Default imports
class AwesomeStrategy(IStrategy):
# ... 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:
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
timeframe=self.timeframe)
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
new_entryprice = dataframe["bollinger_10_lowerband"].iat[-1]
return new_entryprice
@ -542,7 +534,7 @@ class AwesomeStrategy(IStrategy):
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
timeframe=self.timeframe)
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
new_exitprice = dataframe["bollinger_10_upperband"].iat[-1]
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).
``` python
from datetime import datetime, timedelta
from freqtrade.persistence import Trade, Order
# Default imports
class AwesomeStrategy(IStrategy):
@ -588,11 +579,11 @@ class AwesomeStrategy(IStrategy):
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
unfilledtimeout = {
'entry': 60 * 25,
'exit': 60 * 25
"entry": 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:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True
@ -603,7 +594,7 @@ class AwesomeStrategy(IStrategy):
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:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True
@ -620,8 +611,7 @@ class AwesomeStrategy(IStrategy):
### Custom order timeout example (using additional data)
``` python
from datetime import datetime
from freqtrade.persistence import Trade, Order
# Default imports
class AwesomeStrategy(IStrategy):
@ -629,24 +619,24 @@ class AwesomeStrategy(IStrategy):
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
unfilledtimeout = {
'entry': 60 * 25,
'exit': 60 * 25
"entry": 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:
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.
if current_price > order.price * 1.02:
return True
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:
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.
if current_price < order.price * 0.98:
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).
``` python
# Default imports
class AwesomeStrategy(IStrategy):
# ... populate_* methods
@ -689,7 +681,7 @@ class AwesomeStrategy(IStrategy):
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime
: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.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
@ -711,8 +703,7 @@ The exit-reasons (if applicable) will be in the following sequence:
* `trailing_stop_loss`
``` python
from freqtrade.persistence import Trade
# Default imports
class AwesomeStrategy(IStrategy):
@ -738,14 +729,14 @@ class AwesomeStrategy(IStrategy):
or current rate for market orders.
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'exit_signal', 'force_exit', 'emergency_exit']
Can be any of ["roi", "stop_loss", "stoploss_on_exchange", "trailing_stop_loss",
"exit_signal", "force_exit", "emergency_exit"]
: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.
:return bool: When True, then the exit-order is placed on the exchange.
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
# This is just a sample, please adjust to your needs
# (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.
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.
@ -793,7 +784,7 @@ Returning a value more than the above (so remaining stake_amount would become ne
!!! Note "About stake size"
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.
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"
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.
``` python
from freqtrade.persistence import Trade
from typing import Optional, Tuple, Union
# Default imports
class DigDeeperStrategy(IStrategy):
@ -876,7 +865,7 @@ class DigDeeperStrategy(IStrategy):
if current_profit > 0.05 and trade.nr_of_successful_exits == 0:
# 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:
return None
@ -886,7 +875,7 @@ class DigDeeperStrategy(IStrategy):
# Only buy when not actively falling price.
last_candle = dataframe.iloc[-1].squeeze()
previous_candle = dataframe.iloc[-2].squeeze()
if last_candle['close'] < previous_candle['close']:
if last_candle["close"] < previous_candle["close"]:
return None
filled_entries = trade.select_filled_orders(trade.entry_side)
@ -904,7 +893,7 @@ class DigDeeperStrategy(IStrategy):
stake_amount = filled_entries[0].stake_amount
# This then calculates current safety order size
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:
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.
```python
from freqtrade.persistence import Trade
from datetime import timedelta, datetime
# Default imports
class AwesomeStrategy(IStrategy):
@ -977,13 +965,18 @@ class AwesomeStrategy(IStrategy):
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
: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 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.
: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.
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
if order.filled > order.remaining:
return None
@ -991,7 +984,7 @@ class AwesomeStrategy(IStrategy):
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
current_candle = dataframe.iloc[-1].squeeze()
# desired price
return current_candle['sma_200']
return current_candle["sma_200"]
# default: maintain existing order
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.
``` python
# Default imports
class AwesomeStrategy(IStrategy):
def leverage(self, pair: str, current_time: datetime, current_rate: float,
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 max_leverage: Max leverage allowed on this pair
: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 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.
``` python
# Default imports
class AwesomeStrategy(IStrategy):
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()
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

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/)
- [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.
@ -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.
Instead, have a look at the [Storing information](strategy-advanced.md#storing-information-persistent) section.
--8<-- "includes/strategy-imports.md"
## Strategy file loading
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
from pathlib import Path
# Change directory
# 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
project_root = "somedir/freqtrade"
i=0
i = 0
try:
os.chdir(project_root)
assert Path('LICENSE').is_file()
except:
while i<4 and (not Path('LICENSE').is_file()):
os.chdir(Path(Path.cwd(), '../'))
i+=1
project_root = Path.cwd()
if not Path("LICENSE").is_file():
i = 0
while i < 4 and (not Path("LICENSE").is_file()):
os.chdir(Path(Path.cwd(), "../"))
i += 1
project_root = Path.cwd()
except FileNotFoundError:
print("Please define the project root relative to the current directory")
print(Path.cwd())
```
@ -35,6 +38,7 @@ print(Path.cwd())
```python
from freqtrade.configuration import Configuration
# Customize these according to your needs.
# Initialize empty configuration object
@ -58,12 +62,14 @@ pair = "BTC/USDT"
from freqtrade.data.history import load_pair_history
from freqtrade.enums import CandleType
candles = load_pair_history(datadir=data_location,
timeframe=config["timeframe"],
pair=pair,
data_format = "json", # Make sure to update this to your data
candle_type=CandleType.SPOT,
)
candles = load_pair_history(
datadir=data_location,
timeframe=config["timeframe"],
pair=pair,
data_format="json", # Make sure to update this to your data
candle_type=CandleType.SPOT,
)
# Confirm success
print(f"Loaded {len(candles)} rows of data for {pair} from {data_location}")
@ -76,14 +82,16 @@ candles.head()
```python
# Load strategy using values set above
from freqtrade.resolvers import StrategyResolver
from freqtrade.data.dataprovider import DataProvider
from freqtrade.resolvers import StrategyResolver
strategy = StrategyResolver.load_strategy(config)
strategy.dp = DataProvider(config, None, None)
strategy.ft_bot_start()
# Generate buy/sell signals using strategy
df = strategy.analyze_ticker(candles, {'pair': pair})
df = strategy.analyze_ticker(candles, {"pair": pair})
df.tail()
```
@ -102,7 +110,7 @@ df.tail()
```python
# Report results
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()
```
@ -119,10 +127,13 @@ Analyze a trades dataframe (also used below for plotting)
```python
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.
backtest_dir = config["user_data_dir"] / "backtest_results"
# 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 can also point to a specific file
# 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.
stats = load_backtest_stats(backtest_dir)
strategy = 'SampleStrategy'
# All statistics are available per strategy, so if `--strategy-list` was used during backtest, this will be reflected here as well.
strategy = "SampleStrategy"
# All statistics are available per strategy, so if `--strategy-list` was used during backtest,
# this will be reflected here as well.
# Example usages:
print(stats['strategy'][strategy]['results_per_pair'])
print(stats["strategy"][strategy]["results_per_pair"])
# 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)
print(stats['strategy'][strategy]['market_change'])
print(stats["strategy"][strategy]["market_change"])
# Maximum drawdown ()
print(stats['strategy'][strategy]['max_drawdown'])
print(stats["strategy"][strategy]["max_drawdown"])
# Maximum drawdown start and end
print(stats['strategy'][strategy]['drawdown_start'])
print(stats['strategy'][strategy]['drawdown_end'])
print(stats["strategy"][strategy]["drawdown_start"])
print(stats["strategy"][strategy]["drawdown_end"])
# 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
# 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.data.btanalysis import load_backtest_stats
import plotly.express as px
import pandas as pd
# strategy = 'SampleStrategy'
# config = Configuration.from_files(["user_data/config.json"])
# backtest_dir = config["user_data_dir"] / "backtest_results"
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['equity_daily'] = df['equity'].cumsum()
df = pd.DataFrame(columns=["dates", "equity"], data=strategy_stats["daily_profit"])
df["equity_daily"] = df["equity"].cumsum()
fig = px.line(df, x="dates", y="equity_daily")
fig.show()
```
### 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
from freqtrade.data.btanalysis import load_trades_from_db
# Fetch trades from database
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
from freqtrade.data.btanalysis import analyze_trade_parallelism
# Analyze the above
parallel_trades = analyze_trade_parallelism(trades, '5m')
parallel_trades = analyze_trade_parallelism(trades, "5m")
parallel_trades.plot()
```
@ -222,23 +236,23 @@ Freqtrade offers interactive plotting capabilities based on plotly.
```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
# 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
graph = generate_candlestick_graph(pair=pair,
data=data_red,
trades=trades_red,
indicators1=['sma20', 'ema50', 'ema55'],
indicators2=['rsi', 'macd', 'macdsignal', 'macdhist']
)
graph = generate_candlestick_graph(
pair=pair,
data=data_red,
trades=trades_red,
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
graph.show(renderer="browser")
```
## Plot average profit per trade as distribution graph
@ -257,12 +270,12 @@ graph.show(renderer="browser")
```python
import plotly.figure_factory as ff
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.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.

View File

@ -11,3 +11,7 @@
.rst-versions .rst-other-versions {
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
on the exchange. The see the list of all pairs/markets (not only the active ones), use the `-a`/`-all` option.
By default, only active pairs/markets are shown. Active pairs/markets are those that can currently be traded on the exchange.
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.

View File

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

View File

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

View File

@ -132,7 +132,15 @@ ARGS_CONVERT_TRADES = [
"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 = [
"pairs",

View File

@ -446,8 +446,12 @@ AVAILABLE_CLI_OPTIONS = {
),
"download_trades": Arg(
"--dl-trades",
help="Download trades instead of OHLCV data. The bot will resample trades to the "
"desired timeframe as specified as --timeframes/-t.",
help="Download trades instead of OHLCV data.",
action="store_true",
),
"trades": Arg(
"--trades",
help="Work on trades data instead of OHLCV data.",
action="store_true",
),
"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.exceptions import ConfigurationError
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import plural
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
from freqtrade.resolvers import ExchangeResolver
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:
"""
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)
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(
config["datadir"], config.get("trading_mode", TradingMode.SPOT)
)
if args["pairs"]:
paircombs = [comb for comb in paircombs if comb[0] in args["pairs"]]
title = f"Found {len(paircombs)} pair / timeframe combinations."
@ -171,3 +175,51 @@ def start_list_data(args: Dict[str, Any]) -> None:
summary=title,
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"]:
print("\n".join([e["name"] for e in available_exchanges]))
print("\n".join([e["classname"] for e in available_exchanges]))
else:
if args["list_exchanges_all"]:
title = (
@ -46,14 +46,20 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
table = Table(title=title)
table.add_column("Exchange Name")
table.add_column("Class Name")
table.add_column("Markets")
table.add_column("Reason")
for exchange in available_exchanges:
name = Text(exchange["name"])
if exchange["supported"]:
name.append(" (Official)", style="italic")
name.append(" (Supported)", style="italic")
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(
", ".join(
@ -68,6 +74,7 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
table.add_row(
name,
classname,
trade_modes,
exchange["comment"],
style=None if exchange["valid"] else "red",

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import logging
from collections import Counter
from copy import deepcopy
from datetime import datetime
from typing import Any, Dict
from jsonschema import Draft4Validator, validators
@ -201,16 +202,32 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
"""
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:
raise ConfigurationError(
"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:
raise ConfigurationError(
"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 = [
"StaticPairList",
"VolumePairList",
"PercentChangePairList",
"ProducerPairList",
"RemotePairList",
"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)
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()
]
deltas = [len(x) for x in dates]

View File

@ -78,6 +78,8 @@ def populate_dataframe_with_trades(
# create columns for trades
_init_dataframe_with_trades_columns(dataframe)
if trades is None or trades.empty:
return dataframe, cached_grouped_trades
try:
start_time = time.time()
@ -88,7 +90,7 @@ def populate_dataframe_with_trades(
max_candles = config_orderflow["max_candles"]
start_date = dataframe.tail(max_candles).date.iat[0]
# 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)
# group trades by candle start

View File

@ -521,15 +521,12 @@ class DataProvider:
(pair, timeframe or self._config["timeframe"], _candle_type), copy=copy
)
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(
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
else:

View File

@ -12,7 +12,7 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import List, Optional, Tuple, Type
from pandas import DataFrame
from pandas import DataFrame, to_datetime
from freqtrade import misc
from freqtrade.configuration import TimeRange
@ -32,6 +32,7 @@ logger = logging.getLogger(__name__)
class IDataHandler(ABC):
_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:
self._datadir = datadir
@ -166,6 +167,50 @@ class IDataHandler(ABC):
: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
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
:return: List of trades
"""
trades = trades_df_remove_duplicates(
self._trades_load(pair, trading_mode, timerange=timerange)
)
try:
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)
return trades

View File

@ -39,6 +39,7 @@ from freqtrade.exchange.exchange_utils_timeframe import (
from freqtrade.exchange.gate import Gate
from freqtrade.exchange.hitbtc import Hitbtc
from freqtrade.exchange.htx import Htx
from freqtrade.exchange.hyperliquid import Hyperliquid
from freqtrade.exchange.idex import Idex
from freqtrade.exchange.kraken import Kraken
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
from typing import Dict
from ccxt import DECIMAL_PLACES
from freqtrade.exchange import Exchange
@ -22,3 +24,11 @@ class Bitvavo(Exchange):
_ft_has: Dict = {
"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,
)
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 (
chunks,
deep_merge_dicts,
@ -128,6 +135,7 @@ class Exchange:
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
"ohlcv_volume_currency": "base", # "base" or "quote"
"tickers_have_quoteVolume": True,
"tickers_have_percentage": True,
"tickers_have_bid_ask": True, # bid / ask empty for fetch_tickers
"tickers_have_price": True,
"trades_limit": 1000, # Limit for 1 call to fetch_trades
@ -315,7 +323,7 @@ class Exchange:
asyncio.set_event_loop(loop)
return loop
def validate_config(self, config):
def validate_config(self, config: Config) -> None:
# Check if timeframe is available
self.validate_timeframes(config.get("timeframe"))
@ -329,6 +337,7 @@ class Exchange:
self.validate_pricing(config["exit_pricing"])
self.validate_pricing(config["entry_pricing"])
self.validate_orderflow(config["exchange"])
self.validate_freqai(config)
def _init_ccxt(
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")
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"),
"password": exchange_config.get("password"),
"uid": exchange_config.get("uid", ""),
"accountId": exchange_config.get("accountId", ""),
"accountId": exchange_config.get("account_id", exchange_config.get("accountId", "")),
# DEX attributes:
"walletAddress": exchange_config.get("walletAddress"),
"privateKey": exchange_config.get("privateKey"),
"walletAddress": exchange_config.get(
"wallet_address", exchange_config.get("walletAddress")
),
"privateKey": exchange_config.get("private_key", exchange_config.get("privateKey")),
}
if ccxt_kwargs:
logger.info("Applying additional ccxt config: %s", ccxt_kwargs)
@ -411,7 +424,17 @@ class Exchange:
@property
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
def additional_exchange_init(self) -> None:
@ -541,7 +564,7 @@ class Exchange:
else:
return self._trades[pair_interval]
else:
return DataFrame()
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
def get_contract_size(self, pair: str) -> Optional[float]:
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."
)
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:
"""
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.
"""
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:
@ -1645,7 +1678,7 @@ class Exchange:
return order
@retrier
def get_balances(self) -> dict:
def get_balances(self) -> CcxtBalances:
try:
balances = self._api.fetch_balance()
# Remove additional info from ccxt results
@ -1665,7 +1698,7 @@ class Exchange:
raise OperationalException(e) from e
@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.
If no pair is given, all positions are returned.
@ -1677,7 +1710,7 @@ class Exchange:
symbols = []
if 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)
return positions
except ccxt.DDoSProtection as e:
@ -2469,17 +2502,17 @@ class Exchange:
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
# 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 = {}
# 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)
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:
if isinstance(res, Exception):
@ -2607,12 +2640,13 @@ class Exchange:
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(
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}"
) from e
except ccxt.BaseError as e:
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
async def _fetch_funding_rate_history(
@ -2677,6 +2711,94 @@ class Exchange:
self._trades[(pair, timeframe, c_type)] = 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(
self,
pair_list: ListPairsWithTimeframes,
@ -2697,90 +2819,25 @@ class Exchange:
self._config["datadir"], data_format=self._config["dataformat_trades"]
)
logger.debug("Refreshing TRADES data for %d pairs", len(pair_list))
since_ms = None
results_df = {}
for pair, timeframe, candle_type in set(pair_list):
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
trades_dl_jobs = []
for pair_wt in set(pair_list):
trades_dl_jobs.append(self._build_trades_dl_jobs(pair_wt, data_handler, cache))
else:
until = int(timeframe_to_prev_date(timeframe).timestamp()) * 1000
all_stored_ticks_df = data_handler.trades_load(
f"{pair}-cached", self.trading_mode
)
async def gather_coroutines(coro):
return await asyncio.gather(*coro, return_exceptions=True)
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"]
)
for dl_job_chunk in chunks(trades_dl_jobs, 100):
with self._loop_lock:
results = self.loop.run_until_complete(gather_coroutines(dl_job_chunk))
# from_id overrules with exchange set to id paginate
[_, new_ticks] = self.get_historic_trades(
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")
for res in results:
if isinstance(res, Exception):
logger.warning(f"Async code raised an exception: {repr(res)}")
continue
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,
)
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}")
pairwt, trades_df = res
if trades_df is not None:
results_df[pairwt] = trades_df
return results_df

View File

@ -2,6 +2,7 @@
Exchange support utils
"""
import inspect
from datetime import datetime, timedelta, timezone
from math import ceil, floor
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]]
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
"""
try:
@ -64,11 +65,10 @@ def validate_exchange(exchange: str) -> Tuple[bool, str, bool]:
ex_mod = getattr(ccxt.async_support, exchange.lower())()
if not ex_mod or not ex_mod.has:
return False, "", False
return False, "", None
result = True
reason = ""
is_dex = getattr(ex_mod, "dex", False)
missing = [
k
for k, v in EXCHANGE_HAS_REQUIRED.items()
@ -87,19 +87,24 @@ def validate_exchange(exchange: str) -> Tuple[bool, str, bool]:
if 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(
exchange_name: str, exchangeClasses: Dict[str, Any]
) -> ValidExchangesType:
valid, comment, is_dex = validate_exchange(exchange_name)
valid, comment, ex_mod = validate_exchange(exchange_name)
result: ValidExchangesType = {
"name": exchange_name,
"name": getattr(ex_mod, "name", exchange_name),
"classname": exchange_name,
"valid": valid,
"supported": exchange_name.lower() in SUPPORTED_EXCHANGES,
"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": ""}],
}
if resolved := exchangeClasses.get(exchange_name.lower()):

View File

@ -3,6 +3,8 @@
import logging
from typing import Dict
from ccxt import SIGNIFICANT_DIGITS
from freqtrade.exchange import Exchange
@ -17,8 +19,15 @@ class Hyperliquid(Exchange):
_ft_has: Dict = {
# Only the most recent 5000 candles are available according to the
# exchange's API documentation.
"ohlcv_has_history": True,
"ohlcv_has_history": False,
"ohlcv_candle_limit": 5000,
"trades_has_history": False, # Trades endpoint doesn't seem available.
"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.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.types import Tickers
from freqtrade.exchange.types import CcxtBalances, Tickers
logger = logging.getLogger(__name__)
@ -57,7 +57,7 @@ class Kraken(Exchange):
return super().get_tickers(symbols=symbols, cached=cached)
@retrier
def get_balances(self) -> dict:
def get_balances(self) -> CcxtBalances:
if self._config["dry_run"]:
return {}

View File

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

View File

@ -12,9 +12,13 @@ class Ticker(TypedDict):
last: Optional[float]
quoteVolume: Optional[float]
baseVolume: Optional[float]
percentage: Optional[float]
# Several more - only listing required.
Tickers = Dict[str, Ticker]
class OrderBook(TypedDict):
symbol: str
bids: List[Tuple[float, float]]
@ -24,7 +28,24 @@ class OrderBook(TypedDict):
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?,
OHLCVResponse = Tuple[str, str, CandleType, List, bool]

View File

@ -374,6 +374,7 @@ class FreqtradeBot(LoggingMixin):
if trade.exchange != self.exchange.id:
continue
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.price_precision = self.exchange.get_precision_price(trade.pair)
trade.contract_size = self.exchange.get_contract_size(trade.pair)
@ -541,7 +542,11 @@ class FreqtradeBot(LoggingMixin):
)
else:
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 trade.fully_canceled_entry_order_count == len(trade.orders):
logger.warning(
@ -992,6 +997,7 @@ class FreqtradeBot(LoggingMixin):
amount_precision=self.exchange.get_precision_amount(pair),
price_precision=self.exchange.get_precision_price(pair),
precision_mode=self.exchange.precisionMode,
precision_mode_price=self.exchange.precision_mode_price,
contract_size=self.exchange.get_contract_size(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.commands import Arguments
from freqtrade.configuration import asyncio_setup
from freqtrade.constants import DOCS_LINK
from freqtrade.exceptions import ConfigurationError, FreqtradeException, OperationalException
from freqtrade.loggers import setup_logging_pre
@ -33,6 +34,7 @@ def main(sysargv: Optional[List[str]] = None) -> None:
return_code: Any = 1
try:
setup_logging_pre()
asyncio_setup()
arguments = Arguments(sysargv)
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()}
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.
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
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.
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.base_analysis import BaseAnalysis, VarHolder
from freqtrade.resolvers import StrategyResolver
logger = logging.getLogger(__name__)
@ -21,10 +22,19 @@ logger = logging.getLogger(__name__)
class RecursiveAnalysis(BaseAnalysis):
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)
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_lookahead_array: List[VarHolder] = []
@ -58,9 +68,13 @@ class RecursiveAnalysis(BaseAnalysis):
values_diff = compare_df.loc[indicator]
values_diff_self = values_diff.loc["self"]
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:
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))
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_for_bias_tester()

View File

@ -17,9 +17,13 @@ class RecursiveAnalysisSubFunctions:
@staticmethod
def text_table_recursive_analysis_instances(recursive_instances: List[RecursiveAnalysis]):
startups = recursive_instances[0]._startup_candle
strat_scc = recursive_instances[0]._strat_scc
headers = ["Indicators"]
for candle in startups:
headers.append(str(candle))
if candle == strat_scc:
headers.append(f"{candle} (from strategy)")
else:
headers.append(str(candle))
data = []
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)
logger.info(f"Using fee {self.fee:.4%} - worst case fee from exchange (lowest tier).")
self.precision_mode = self.exchange.precisionMode
self.precision_mode_price = self.exchange.precision_mode_price
if self.config.get("freqai_backtest_live_models", False):
from freqtrade.freqai.utils import get_timerange_backtest_live_models
@ -329,15 +330,15 @@ class Backtesting:
else:
self.detail_data = {}
if self.trading_mode == TradingMode.FUTURES:
self.funding_fee_timeframe: str = self.exchange.get_option("funding_fee_timeframe")
self.funding_fee_timeframe_secs: int = timeframe_to_seconds(self.funding_fee_timeframe)
funding_fee_timeframe: str = self.exchange.get_option("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")
# Load additional futures data.
funding_rates_dict = history.load_data(
datadir=self.config["datadir"],
pairs=self.pairlists.whitelist,
timeframe=self.funding_fee_timeframe,
timeframe=funding_fee_timeframe,
timerange=self.timerange,
startup_candles=0,
fail_without_data=True,
@ -785,7 +786,7 @@ class Backtesting:
)
if rate is not None and rate != close_rate:
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.
# 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)
# which freqtrade does not support in live.
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":
propose_rate = max(propose_rate, row[LOW_IDX])
else:
@ -1109,6 +1112,7 @@ class Backtesting:
amount_precision=precision_amount,
price_precision=precision_price,
precision_mode=self.precision_mode,
precision_mode_price=self.precision_mode_price,
contract_size=contract_size,
orders=[],
)
@ -1332,10 +1336,9 @@ class Backtesting:
pair: str,
current_time: datetime,
end_date: datetime,
open_trade_count_start: int,
trade_dir: Optional[LongShort],
is_first: bool = True,
) -> int:
) -> None:
"""
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
if self.manage_open_orders(t, current_time, row):
# Close trade
open_trade_count_start -= 1
LocalTrade.remove_bt_trade(t)
self.wallets.update()
@ -1361,13 +1363,9 @@ class Backtesting:
and trade_dir is not None
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)
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()
else:
self._collate_rejected(pair, row)
@ -1386,7 +1384,28 @@ class Backtesting:
order = trade.select_order(trade.exit_side, is_open=True)
if order:
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]:
"""
@ -1411,87 +1430,75 @@ class Backtesting:
# Indexes per pair, so some pairs are allowed to have a missing start.
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
while current_time <= end_date:
open_trade_count_start = LocalTrade.bt_open_open_trade_count
self.check_abort()
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
current_time=current_time
)
for i, pair in enumerate(data):
row_index = indexes[pair]
row = self.validate_row(data, pair, row_index, current_time)
if not row:
for current_time, pair, is_first in self.time_pair_generator(
start_date, end_date, self.timeframe_td, list(data.keys())
):
if is_first:
self.check_abort()
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
current_time=current_time
)
row_index = indexes[pair]
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
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
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
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)
self.backtest_loop(
det_row,
pair,
current_time_det,
end_date,
trade_dir,
is_first,
)
# Move time one configured time_interval ahead.
self.progress.increment()
current_time += self.timeframe_td
current_time_det += self.timeframe_detail_td
is_first = False
else:
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.wallets.update()
results = trade_list_to_dataframe(LocalTrade.trades)
results = trade_list_to_dataframe(LocalTrade.bt_trades)
return {
"results": results,
"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.externals import cloudpickle
from pandas import DataFrame
from rich.align import Align
from rich.console import Console
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.dimensions: List[Dimension] = []
self._hyper_out: HyperoptOutput = HyperoptOutput()
self._hyper_out: HyperoptOutput = HyperoptOutput(streaming=True)
self.config = config
self.min_date: datetime
@ -168,7 +167,9 @@ class Hyperopt:
cloudpickle.register_pickle_by_value(sys.modules[modules.__module__])
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
# the number of parameters in the list.
if len(raw_params) != len(dimensions):
@ -317,7 +318,7 @@ class Hyperopt:
+ 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
"""
@ -404,7 +405,12 @@ class Hyperopt:
)
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]:
params_details = self._get_params_details(params_dict)
@ -628,7 +634,7 @@ class Hyperopt:
# Define progressbar
with get_progress_tracker(
console=console,
cust_objs=[Align.center(self._hyper_out.table)],
cust_callables=[self._hyper_out],
) as pbar:
task = pbar.add_task("Epochs", total=self.total_epochs)

View File

@ -1,6 +1,8 @@
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.table import Table
from rich.text import Text
@ -11,7 +13,16 @@ from freqtrade.util import fmt_coin
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(
title="Hyperopt results",
)
@ -26,17 +37,6 @@ class HyperoptOutput:
self.table.add_column("Objective", 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):
if not console:
console = Console(
@ -55,8 +55,28 @@ class HyperoptOutput:
) -> None:
"""Format one or multiple rows and add them"""
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(
*[
# "Best":

View File

@ -147,6 +147,9 @@ def migrate_trades_and_orders_table(
price_precision = get_column_def(cols, "price_precision", "null")
precision_mode = get_column_def(cols, "precision_mode", "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
with engine.begin() as connection:
@ -177,7 +180,7 @@ def migrate_trades_and_orders_table(
timeframe, open_trade_value, close_profit_abs,
trading_mode, leverage, liquidation_price, is_short,
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
)
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,
{realized_profit} realized_profit,
{amount_precision} amount_precision, {price_precision} price_precision,
{precision_mode} precision_mode, {contract_size} contract_size,
{max_stake_amount} max_stake_amount
{precision_mode} precision_mode, {precision_mode_price} precision_mode_price,
{contract_size} contract_size, {max_stake_amount} max_stake_amount
from {trade_back_name}
"""
)
@ -348,8 +351,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# if ('orders' not in previous_tables
# or not has_column(cols_orders, 'funding_fee')):
migrating = False
# if not has_column(cols_trades, 'funding_fee_running'):
if not has_column(cols_orders, "ft_order_tag"):
if not has_column(cols_trades, "precision_mode_price"):
# if not has_column(cols_orders, "ft_order_tag"):
migrating = True
logger.info(
f"Running database migration for trades - "

View File

@ -373,12 +373,12 @@ class LocalTrade:
use_db: bool = False
# Trades container for backtesting
trades: List["LocalTrade"] = []
trades_open: List["LocalTrade"] = []
bt_trades: List["LocalTrade"] = []
bt_trades_open: List["LocalTrade"] = []
# Copy of trades_open - but indexed by pair
bt_trades_open_pp: Dict[str, List["LocalTrade"]] = defaultdict(list)
bt_open_open_trade_count: int = 0
total_profit: float = 0
bt_total_profit: float = 0
realized_profit: float = 0
id: int = 0
@ -433,6 +433,7 @@ class LocalTrade:
amount_precision: Optional[float] = None
price_precision: Optional[float] = None
precision_mode: Optional[int] = None
precision_mode_price: Optional[int] = None
contract_size: Optional[float] = None
# Leverage trading properties
@ -730,6 +731,7 @@ class LocalTrade:
"amount_precision": self.amount_precision,
"price_precision": self.price_precision,
"precision_mode": self.precision_mode,
"precision_mode_price": self.precision_mode_price,
"contract_size": self.contract_size,
"has_open_orders": self.has_open_orders,
"orders": orders_json,
@ -740,11 +742,11 @@ class LocalTrade:
"""
Resets all trades. Only active for backtesting mode.
"""
LocalTrade.trades = []
LocalTrade.trades_open = []
LocalTrade.bt_trades = []
LocalTrade.bt_trades_open = []
LocalTrade.bt_trades_open_pp = defaultdict(list)
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:
"""
@ -810,7 +812,7 @@ class LocalTrade:
stop_loss_norm = price_to_precision(
new_loss,
self.price_precision,
self.precision_mode,
self.precision_mode_price,
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP,
)
# no stop loss assigned yet
@ -819,7 +821,7 @@ class LocalTrade:
self.initial_stop_loss = price_to_precision(
stop_loss_norm,
self.price_precision,
self.precision_mode,
self.precision_mode_price,
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP,
)
self.initial_stop_loss_pct = -1 * abs(stoploss)
@ -1217,7 +1219,7 @@ class LocalTrade:
# with realized_profit.
close_profit = (close_profit_abs / total_stake) * self.leverage
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
self.funding_fees = funding_fees
self.max_stake_amount = float(max_stake_amount)
@ -1236,7 +1238,7 @@ class LocalTrade:
self.open_rate = float(current_stake / current_amount)
self.amount = current_amount_tr
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()
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)
@ -1405,7 +1407,7 @@ class LocalTrade:
Helper function to query Trades.
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 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 is_open: Filter by open/closed status
@ -1418,13 +1420,13 @@ class LocalTrade:
# Offline mode - without database
if is_open is not None:
if is_open:
sel_trades = LocalTrade.trades_open
sel_trades = LocalTrade.bt_trades_open
else:
sel_trades = LocalTrade.trades
sel_trades = LocalTrade.bt_trades
else:
# 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:
sel_trades = [trade for trade in sel_trades if trade.pair == pair]
@ -1439,24 +1441,24 @@ class LocalTrade:
@staticmethod
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_open_open_trade_count -= 1
LocalTrade.trades.append(trade)
LocalTrade.total_profit += trade.close_profit_abs
LocalTrade.bt_trades.append(trade)
LocalTrade.bt_total_profit += trade.close_profit_abs
@staticmethod
def add_bt_trade(trade):
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_open_open_trade_count += 1
else:
LocalTrade.trades.append(trade)
LocalTrade.bt_trades.append(trade)
@staticmethod
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_open_open_trade_count -= 1
@ -1562,6 +1564,7 @@ class LocalTrade:
amount_precision=data.get("amount_precision", None),
price_precision=data.get("price_precision", 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),
)
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
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
# Leverage trading properties
@ -1761,7 +1767,7 @@ class Trade(ModelBase, LocalTrade):
Helper function to query Trades.j
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 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]
"""

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
"""
return f"Cooldown period for {self.stop_duration_str}."
return f"Cooldown period for {self.unlock_reason_time_element}."
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]:
"""
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 = [
# Trade.is_open.is_(False),
# Trade.close_date > look_back_until,
@ -42,8 +42,8 @@ class CooldownPeriod(IProtection):
# Get latest trade
# Ignore type error as we know we only get closed trades.
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)
until = self.calculate_lock_end([trade], self._stop_duration)
self.log_once(f"Cooldown for {pair} {self.unlock_reason_time_element}.", logger.info)
until = self.calculate_lock_end([trade])
return ProtectionReturn(
lock=True,

View File

@ -32,15 +32,19 @@ class IProtection(LoggingMixin, ABC):
self._config = config
self._protection_config = protection_config
self._stop_duration_candles: Optional[int] = None
self._stop_duration: int = 0
self._lookback_period_candles: Optional[int] = None
self._unlock_at: Optional[str] = None
tf_in_min = timeframe_to_minutes(config["timeframe"])
if "stop_duration_candles" in protection_config:
self._stop_duration_candles = int(protection_config.get("stop_duration_candles", 1))
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:
self._stop_duration_candles = None
self._stop_duration = int(protection_config.get("stop_duration", 60))
if "lookback_period_candles" in protection_config:
self._lookback_period_candles = int(protection_config.get("lookback_period_candles", 1))
self._lookback_period = tf_in_min * self._lookback_period_candles
@ -80,6 +84,16 @@ class IProtection(LoggingMixin, ABC):
else:
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
def short_desc(self) -> str:
"""
@ -105,16 +119,23 @@ class IProtection(LoggingMixin, ABC):
If true, this pair will be locked with <reason> until <until>
"""
@staticmethod
def calculate_lock_end(trades: List[LocalTrade], stop_minutes: int) -> datetime:
def calculate_lock_end(self, trades: List[LocalTrade]) -> datetime:
"""
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])
# coming from Database, tzinfo is not set.
if max_date.tzinfo is None:
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

View File

@ -36,7 +36,7 @@ class LowProfitPairs(IProtection):
"""
return (
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(
@ -70,7 +70,7 @@ class LowProfitPairs(IProtection):
f"within {self._lookback_period} minutes.",
logger.info,
)
until = self.calculate_lock_end(trades, self._stop_duration)
until = self.calculate_lock_end(trades)
return ProtectionReturn(
lock=True,

View File

@ -39,7 +39,7 @@ class MaxDrawdown(IProtection):
"""
return (
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]:
@ -70,7 +70,8 @@ class MaxDrawdown(IProtection):
f" within {self.lookback_period_str}.",
logger.info,
)
until = self.calculate_lock_end(trades, self._stop_duration)
until = self.calculate_lock_end(trades)
return ProtectionReturn(
lock=True,

View File

@ -38,7 +38,7 @@ class StoplossGuard(IProtection):
"""
return (
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(
@ -78,7 +78,7 @@ class StoplossGuard(IProtection):
f"stoplosses within {self._lookback_period} minutes.",
logger.info,
)
until = self.calculate_lock_end(trades, self._stop_duration)
until = self.calculate_lock_end(trades)
return ProtectionReturn(
lock=True,
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)
),
"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",
}

View File

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

View File

@ -58,7 +58,7 @@ async def channel_broadcaster(channel: WebSocketChannel, message_stream: Message
" 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):

View File

@ -8,6 +8,7 @@ def asyncio_setup() -> None: # pragma: no cover
# Set eventloop for win32 setups
# Reverts a change done in uvicorn 0.15.0 - which now sets the eventloop
# via policy.
# TODO: is this workaround actually needed?
import sys
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)
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
@ -88,7 +88,7 @@ class WebSocketChannel:
disconnect the connection.
: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:
_ = time.time()
@ -96,7 +96,8 @@ class WebSocketChannel:
# a TimeoutError and bubble up to the
# message_endpoint to close the connection
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() - _
self._send_times.append(total_time)

View File

@ -296,7 +296,10 @@ class RPC:
else:
trade_profit = 0.0
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:
fiat_profit = self._fiat_converter.convert_amount(
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,
"stake": stake_currency,
"side": "long",
"leverage": 1,
"position": 0,
"is_bot_managed": is_bot_managed,
"is_position": False,
@ -764,7 +766,6 @@ class RPC:
"est_stake": position.collateral,
"est_stake_bot": position.collateral,
"stake": stake_currency,
"leverage": position.leverage,
"side": position.side,
"is_bot_managed": True,
"is_position": True,

View File

@ -1133,7 +1133,6 @@ class Telegram(RPCHandler):
curr_output = (
f"*{curr['currency']}:*\n"
f"\t`{curr['side']}: {curr['position']:.8f}`\n"
f"\t`Leverage: {curr['leverage']:.1f}`\n"
f"\t`Est. {curr['stake']}: "
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_seconds,
)
from freqtrade.persistence import Order, PairLocks, Trade
from freqtrade.strategy.informative_decorator import informative
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.parameters import (
@ -20,3 +21,27 @@ from freqtrade.strategy.strategy_helper import (
stoploss_from_absolute,
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
}' %}
{
"$schema": "https://schema.freqtrade.io/schema.json",
"max_open_trades": {{ max_open_trades }},
"stake_currency": "{{ stake_currency }}",
"stake_amount": {{ stake_amount }},

View File

@ -1,15 +1,34 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these libs ---
# --- Do not remove these imports ---
import numpy as np
import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame
from datetime import datetime
from typing import Optional, Union
from typing import Dict, Optional, Union, Tuple
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
IntParameter, IStrategy, merge_informative_pair)
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
@ -40,7 +59,7 @@ class {{ strategy }}(IStrategy):
INTERFACE_VERSION = 3
# Optimal timeframe for the strategy.
timeframe = '5m'
timeframe = "5m"
# Can this strategy go short?
can_short: bool = False
@ -78,8 +97,8 @@ class {{ strategy }}(IStrategy):
buy_rsi = IntParameter(10, 40, default=30, space="buy")
sell_rsi = IntParameter(60, 90, default=70, space="sell")
{{ attributes | indent(4) }}
{{ plot_config | indent(4) }}
{{- attributes | indent(4) }}
{{- plot_config | indent(4) }}
def informative_pairs(self):
"""
@ -105,7 +124,7 @@ class {{ strategy }}(IStrategy):
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""
{{ indicators | indent(8) }}
{{- indicators | indent(8) }}
return dataframe
@ -119,9 +138,9 @@ class {{ strategy }}(IStrategy):
dataframe.loc[
(
{{ 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)
"""
dataframe.loc[
@ -144,9 +163,9 @@ class {{ strategy }}(IStrategy):
dataframe.loc[
(
{{ 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)
"""
dataframe.loc[
@ -157,4 +176,4 @@ class {{ strategy }}(IStrategy):
'exit_short'] = 1
"""
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
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these libs ---
import numpy as np # noqa
import pandas as pd # noqa
# --- 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 Optional, Union
from typing import Dict, Optional, Union, Tuple
from freqtrade.strategy import (
IStrategy,
Trade,
Order,
PairLocks,
informative, # @informative decorator
# Hyperopt Parameters
BooleanParameter,
CategoricalParameter,
DecimalParameter,
IStrategy,
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
import freqtrade.vendor.qtpylib.indicators as qtpylib
from technical import qtpylib
# This class is a sample. Feel free to customize it.

View File

@ -29,19 +29,22 @@
"import os\n",
"from pathlib import Path\n",
"\n",
"\n",
"# Change directory\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",
"project_root = \"somedir/freqtrade\"\n",
"i=0\n",
"i = 0\n",
"try:\n",
" os.chdir(project_root)\n",
" assert Path('LICENSE').is_file()\n",
"except:\n",
" while i<4 and (not Path('LICENSE').is_file()):\n",
" os.chdir(Path(Path.cwd(), '../'))\n",
" i+=1\n",
" project_root = Path.cwd()\n",
" if not Path(\"LICENSE\").is_file():\n",
" i = 0\n",
" while i < 4 and (not Path(\"LICENSE\").is_file()):\n",
" os.chdir(Path(Path.cwd(), \"../\"))\n",
" i += 1\n",
" project_root = Path.cwd()\n",
"except FileNotFoundError:\n",
" print(\"Please define the project root relative to the current directory\")\n",
"print(Path.cwd())"
]
},
@ -60,6 +63,7 @@
"source": [
"from freqtrade.configuration import Configuration\n",
"\n",
"\n",
"# Customize these according to your needs.\n",
"\n",
"# Initialize empty configuration object\n",
@ -87,12 +91,14 @@
"from freqtrade.data.history import load_pair_history\n",
"from freqtrade.enums import CandleType\n",
"\n",
"candles = load_pair_history(datadir=data_location,\n",
" timeframe=config[\"timeframe\"],\n",
" pair=pair,\n",
" data_format = \"json\", # Make sure to update this to your data\n",
" candle_type=CandleType.SPOT,\n",
" )\n",
"\n",
"candles = load_pair_history(\n",
" datadir=data_location,\n",
" timeframe=config[\"timeframe\"],\n",
" pair=pair,\n",
" data_format=\"json\", # Make sure to update this to your data\n",
" candle_type=CandleType.SPOT,\n",
")\n",
"\n",
"# Confirm success\n",
"print(f\"Loaded {len(candles)} rows of data for {pair} from {data_location}\")\n",
@ -114,14 +120,16 @@
"outputs": [],
"source": [
"# Load strategy using values set above\n",
"from freqtrade.resolvers import StrategyResolver\n",
"from freqtrade.data.dataprovider import DataProvider\n",
"from freqtrade.resolvers import StrategyResolver\n",
"\n",
"\n",
"strategy = StrategyResolver.load_strategy(config)\n",
"strategy.dp = DataProvider(config, None, None)\n",
"strategy.ft_bot_start()\n",
"\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()"
]
},
@ -148,7 +156,7 @@
"source": [
"# Report results\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()"
]
},
@ -179,10 +187,13 @@
"source": [
"from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats\n",
"\n",
"\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 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",
"stats = load_backtest_stats(backtest_dir)\n",
"\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",
"strategy = \"SampleStrategy\"\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",
"print(stats['strategy'][strategy]['results_per_pair'])\n",
"print(stats[\"strategy\"][strategy][\"results_per_pair\"])\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",
"print(stats['strategy'][strategy]['market_change'])\n",
"print(stats[\"strategy\"][strategy][\"market_change\"])\n",
"# Maximum drawdown ()\n",
"print(stats['strategy'][strategy]['max_drawdown'])\n",
"print(stats[\"strategy\"][strategy][\"max_drawdown\"])\n",
"# Maximum drawdown start and end\n",
"print(stats['strategy'][strategy]['drawdown_start'])\n",
"print(stats['strategy'][strategy]['drawdown_end'])\n",
"print(stats[\"strategy\"][strategy][\"drawdown_start\"])\n",
"print(stats[\"strategy\"][strategy][\"drawdown_end\"])\n",
"\n",
"\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": [
"# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)\n",
"\n",
"import pandas as pd\n",
"import plotly.express as px\n",
"\n",
"from freqtrade.configuration import Configuration\n",
"from freqtrade.data.btanalysis import load_backtest_stats\n",
"import plotly.express as px\n",
"import pandas as pd\n",
"\n",
"\n",
"# strategy = 'SampleStrategy'\n",
"# config = Configuration.from_files([\"user_data/config.json\"])\n",
"# backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n",
"\n",
"stats = load_backtest_stats(backtest_dir)\n",
"strategy_stats = stats['strategy'][strategy]\n",
"strategy_stats = stats[\"strategy\"][strategy]\n",
"\n",
"df = pd.DataFrame(columns=['dates','equity'], data=strategy_stats['daily_profit'])\n",
"df['equity_daily'] = df['equity'].cumsum()\n",
"df = pd.DataFrame(columns=[\"dates\", \"equity\"], data=strategy_stats[\"daily_profit\"])\n",
"df[\"equity_daily\"] = df[\"equity\"].cumsum()\n",
"\n",
"fig = px.line(df, x=\"dates\", y=\"equity_daily\")\n",
"fig.show()\n"
"fig.show()"
]
},
{
@ -278,6 +292,7 @@
"source": [
"from freqtrade.data.btanalysis import load_trades_from_db\n",
"\n",
"\n",
"# Fetch trades from database\n",
"trades = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n",
"\n",
@ -303,8 +318,9 @@
"source": [
"from freqtrade.data.btanalysis import analyze_trade_parallelism\n",
"\n",
"\n",
"# Analyze the above\n",
"parallel_trades = analyze_trade_parallelism(trades, '5m')\n",
"parallel_trades = analyze_trade_parallelism(trades, \"5m\")\n",
"\n",
"parallel_trades.plot()"
]
@ -324,22 +340,23 @@
"metadata": {},
"outputs": [],
"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",
"\n",
"# Filter trades to one pair\n",
"trades_red = trades.loc[trades['pair'] == pair]\n",
"trades_red = trades.loc[trades[\"pair\"] == pair]\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",
"graph = generate_candlestick_graph(pair=pair,\n",
" data=data_red,\n",
" trades=trades_red,\n",
" indicators1=['sma20', 'ema50', 'ema55'],\n",
" indicators2=['rsi', 'macd', 'macdsignal', 'macdhist']\n",
" )\n",
"\n",
"\n"
"graph = generate_candlestick_graph(\n",
" pair=pair,\n",
" data=data_red,\n",
" trades=trades_red,\n",
" indicators1=[\"sma20\", \"ema50\", \"ema55\"],\n",
" indicators2=[\"rsi\", \"macd\", \"macdsignal\", \"macdhist\"],\n",
")"
]
},
{
@ -352,7 +369,7 @@
"# graph.show()\n",
"\n",
"# Render graph in a separate window\n",
"graph.show(renderer=\"browser\")\n"
"graph.show(renderer=\"browser\")"
]
},
{
@ -370,11 +387,12 @@
"source": [
"import plotly.figure_factory as ff\n",
"\n",
"\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",
"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
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
(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["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
dataframe['adx'] = ta.ADX(dataframe)
dataframe["adx"] = ta.ADX(dataframe)
# # Plus Directional Indicator / Movement
# dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
# dataframe['plus_di'] = ta.PLUS_DI(dataframe)
# dataframe["plus_dm"] = ta.PLUS_DM(dataframe)
# dataframe["plus_di"] = ta.PLUS_DI(dataframe)
# # Minus Directional Indicator / Movement
# dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
# dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# dataframe["minus_dm"] = ta.MINUS_DM(dataframe)
# dataframe["minus_di"] = ta.MINUS_DI(dataframe)
# # Aroon, Aroon Oscillator
# aroon = ta.AROON(dataframe)
# dataframe['aroonup'] = aroon['aroonup']
# dataframe['aroondown'] = aroon['aroondown']
# dataframe['aroonosc'] = ta.AROONOSC(dataframe)
# dataframe["aroonup"] = aroon["aroonup"]
# dataframe["aroondown"] = aroon["aroondown"]
# dataframe["aroonosc"] = ta.AROONOSC(dataframe)
# # Awesome Oscillator
# dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
# dataframe["ao"] = qtpylib.awesome_oscillator(dataframe)
# # Keltner Channel
# keltner = qtpylib.keltner_channel(dataframe)
@ -36,58 +36,58 @@ dataframe['adx'] = ta.ADX(dataframe)
# )
# # Ultimate Oscillator
# dataframe['uo'] = ta.ULTOSC(dataframe)
# dataframe["uo"] = ta.ULTOSC(dataframe)
# # Commodity Channel Index: values [Oversold:-100, Overbought:100]
# dataframe['cci'] = ta.CCI(dataframe)
# dataframe["cci"] = ta.CCI(dataframe)
# 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)
# rsi = 0.1 * (dataframe['rsi'] - 50)
# dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1)
# rsi = 0.1 * (dataframe["rsi"] - 50)
# 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)
# dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# dataframe["fisher_rsi_norma"] = 50 * (dataframe["fisher_rsi"] + 1)
# # Stochastic Slow
# stoch = ta.STOCH(dataframe)
# dataframe['slowd'] = stoch['slowd']
# dataframe['slowk'] = stoch['slowk']
# dataframe["slowd"] = stoch["slowd"]
# dataframe["slowk"] = stoch["slowk"]
# Stochastic Fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
dataframe["fastd"] = stoch_fast["fastd"]
dataframe["fastk"] = stoch_fast["fastk"]
# # Stochastic RSI
# 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.
# stoch_rsi = ta.STOCHRSI(dataframe)
# dataframe['fastd_rsi'] = stoch_rsi['fastd']
# dataframe['fastk_rsi'] = stoch_rsi['fastk']
# dataframe["fastd_rsi"] = stoch_rsi["fastd"]
# dataframe["fastk_rsi"] = stoch_rsi["fastk"]
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
dataframe["macd"] = macd["macd"]
dataframe["macdsignal"] = macd["macdsignal"]
dataframe["macdhist"] = macd["macdhist"]
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
dataframe["mfi"] = ta.MFI(dataframe)
# # ROC
# dataframe['roc'] = ta.ROC(dataframe)
# dataframe["roc"] = ta.ROC(dataframe)
# Overlap Studies
# ------------------------------------
# Bollinger Bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
dataframe["bb_lowerband"] = bollinger["lower"]
dataframe["bb_middleband"] = bollinger["mid"]
dataframe["bb_upperband"] = bollinger["upper"]
dataframe["bb_percent"] = (
(dataframe["close"] - dataframe["bb_lowerband"]) /
(dataframe["bb_upperband"] - dataframe["bb_lowerband"])
@ -112,95 +112,95 @@ dataframe["bb_width"] = (
# )
# # EMA - Exponential Moving Average
# dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
# dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
# dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
# dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21)
# dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
# dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# dataframe["ema3"] = ta.EMA(dataframe, timeperiod=3)
# dataframe["ema5"] = ta.EMA(dataframe, timeperiod=5)
# dataframe["ema10"] = ta.EMA(dataframe, timeperiod=10)
# dataframe["ema21"] = ta.EMA(dataframe, timeperiod=21)
# dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
# dataframe["ema100"] = ta.EMA(dataframe, timeperiod=100)
# # SMA - Simple Moving Average
# dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3)
# dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5)
# dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10)
# dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21)
# dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50)
# dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100)
# dataframe["sma3"] = ta.SMA(dataframe, timeperiod=3)
# dataframe["sma5"] = ta.SMA(dataframe, timeperiod=5)
# dataframe["sma10"] = ta.SMA(dataframe, timeperiod=10)
# dataframe["sma21"] = ta.SMA(dataframe, timeperiod=21)
# dataframe["sma50"] = ta.SMA(dataframe, timeperiod=50)
# dataframe["sma100"] = ta.SMA(dataframe, timeperiod=100)
# Parabolic SAR
dataframe['sar'] = ta.SAR(dataframe)
dataframe["sar"] = ta.SAR(dataframe)
# TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator
# ------------------------------------
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
dataframe["htsine"] = hilbert["sine"]
dataframe["htleadsine"] = hilbert["leadsine"]
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
# # Hammer: values [0, 100]
# dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# dataframe["CDLHAMMER"] = ta.CDLHAMMER(dataframe)
# # Inverted Hammer: values [0, 100]
# dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# dataframe["CDLINVERTEDHAMMER"] = ta.CDLINVERTEDHAMMER(dataframe)
# # Dragonfly Doji: values [0, 100]
# dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# dataframe["CDLDRAGONFLYDOJI"] = ta.CDLDRAGONFLYDOJI(dataframe)
# # 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]
# dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# dataframe["CDLMORNINGSTAR"] = ta.CDLMORNINGSTAR(dataframe) # 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
# ------------------------------------
# # Hanging Man: values [0, 100]
# dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# dataframe["CDLHANGINGMAN"] = ta.CDLHANGINGMAN(dataframe)
# # Shooting Star: values [0, 100]
# dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# dataframe["CDLSHOOTINGSTAR"] = ta.CDLSHOOTINGSTAR(dataframe)
# # Gravestone Doji: values [0, 100]
# dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# dataframe["CDLGRAVESTONEDOJI"] = ta.CDLGRAVESTONEDOJI(dataframe)
# # Dark Cloud Cover: values [0, 100]
# dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# dataframe["CDLDARKCLOUDCOVER"] = ta.CDLDARKCLOUDCOVER(dataframe)
# # Evening Doji Star: values [0, 100]
# dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# dataframe["CDLEVENINGDOJISTAR"] = ta.CDLEVENINGDOJISTAR(dataframe)
# # Evening Star: values [0, 100]
# dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
# dataframe["CDLEVENINGSTAR"] = ta.CDLEVENINGSTAR(dataframe)
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
# # Three Line Strike: values [0, -100, 100]
# dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# dataframe["CDL3LINESTRIKE"] = ta.CDL3LINESTRIKE(dataframe)
# # 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]
# dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# dataframe["CDLENGULFING"] = ta.CDLENGULFING(dataframe) # 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]
# 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]
# dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
# dataframe["CDL3INSIDE"] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
# # Chart type
# # ------------------------------------
# # Heikin Ashi Strategy
# heikinashi = qtpylib.heikinashi(dataframe)
# dataframe['ha_open'] = heikinashi['open']
# dataframe['ha_close'] = heikinashi['close']
# dataframe['ha_high'] = heikinashi['high']
# dataframe['ha_low'] = heikinashi['low']
# dataframe["ha_open"] = heikinashi["open"]
# dataframe["ha_close"] = heikinashi["close"]
# dataframe["ha_high"] = heikinashi["high"]
# dataframe["ha_low"] = heikinashi["low"]
# Retrieve best bid and best ask from the orderbook
# ------------------------------------
"""
# first check if dataprovider is available
if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'):
ob = self.dp.orderbook(metadata['pair'], 1)
dataframe['best_bid'] = ob['bids'][0][0]
dataframe['best_ask'] = ob['asks'][0][0]
if self.dp.runmode.value in ("live", "dry_run"):
ob = self.dp.orderbook(metadata["pair"], 1)
dataframe["best_bid"] = ob["bids"][0][0]
dataframe["best_ask"] = ob["asks"][0][0]
"""

View File

@ -3,15 +3,15 @@
# ------------------------------------
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
dataframe["rsi"] = ta.RSI(dataframe)
# Retrieve best bid and best ask from the orderbook
# ------------------------------------
"""
# first check if dataprovider is available
if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'):
ob = self.dp.orderbook(metadata['pair'], 1)
dataframe['best_bid'] = ob['bids'][0][0]
dataframe['best_ask'] = ob['asks'][0][0]
if self.dp.runmode.value in ("live", "dry_run"):
ob = self.dp.orderbook(metadata["pair"], 1)
dataframe["best_bid"] = ob["bids"][0][0]
dataframe["best_ask"] = ob["asks"][0][0]
"""

View File

@ -3,18 +3,18 @@
def plot_config(self):
return {
# Main plot indicators (Moving averages, ...)
'main_plot': {
'tema': {},
'sar': {'color': 'white'},
"main_plot": {
"tema": {},
"sar": {"color": "white"},
},
'subplots': {
"subplots": {
# Subplots - each dict defines one additional plot
"MACD": {
'macd': {'color': 'blue'},
'macdsignal': {'color': 'orange'},
"macd": {"color": "blue"},
"macdsignal": {"color": "orange"},
},
"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
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
(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["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.
order_types = {
'entry': 'limit',
'exit': 'limit',
'stoploss': 'market',
'stoploss_on_exchange': False
"entry": "limit",
"exit": "limit",
"stoploss": "market",
"stoploss_on_exchange": False
}
# Optional order time in force.
order_time_in_force = {
'entry': 'GTC',
'exit': 'GTC'
"entry": "GTC",
"exit": "GTC"
}

View File

@ -13,9 +13,9 @@ def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
"""
pass
def custom_entry_price(self, pair: str, trade: Optional['Trade'],
current_time: 'datetime', proposed_rate: float,
entry_tag: 'Optional[str]', side: str, **kwargs) -> 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:
"""
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
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,
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
def custom_exit_price(self, pair: str, trade: 'Trade',
current_time: 'datetime', proposed_rate: float,
def custom_exit_price(self, pair: str, trade: Trade,
current_time: datetime, proposed_rate: float,
current_profit: float, exit_tag: Optional[str], **kwargs) -> float:
"""
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
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:
"""
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
"""
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]':
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
"""
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
@ -177,9 +177,9 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
"""
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,
current_time: 'datetime', **kwargs) -> bool:
current_time: datetime, **kwargs) -> bool:
"""
Called right before placing a regular exit order.
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
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:
"""
Check entry timeout function callback.
@ -228,7 +228,7 @@ def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
"""
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:
"""
Check exit timeout function callback.
@ -250,7 +250,7 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
"""
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,
min_stake: Optional[float], max_stake: 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
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:
"""
Called right after an order fills.

View File

@ -1,5 +1,5 @@
# Used for list-exchanges
from typing import List
from typing import List, Optional
from typing_extensions import TypedDict
@ -11,8 +11,11 @@ class TradeModeType(TypedDict):
class ValidExchangesType(TypedDict):
name: str
classname: str
valid: bool
supported: bool
comment: str
dex: bool
is_alias: bool
alias_for: Optional[str]
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.progress import 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_callables = cust_callables
super().__init__(*args, **kwargs)
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

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]
table.add_row(*row_to_add)
console = Console(
width=200 if "pytest" in sys.modules else None,
)
width = None
if any(module in ["pytest", "ipykernel"] for module in sys.modules):
width = 200
console = Console(width=width)
console.print(table)
@ -71,7 +73,9 @@ def print_df_rich_table(
row = [_format_value(x, floatfmt=".3f") for x in value_list]
table.add_row(*row)
console = Console(
width=200 if "pytest" in sys.modules else None,
)
width = None
if any(module in ["pytest", "ipykernel"] for module in sys.modules):
width = 200
console = Console(width=width)
console.print(table)

View File

@ -29,7 +29,7 @@ class Wallet(NamedTuple):
class PositionWallet(NamedTuple):
symbol: str
position: float = 0
leverage: float = 0
leverage: Optional[float] = 0 # Don't use this - it's not guaranteed to be set
collateral: float = 0
side: str = "long"
@ -66,6 +66,17 @@ class Wallets:
else:
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:
"""
Update from database in dry-run mode
@ -82,7 +93,7 @@ class Wallets:
tot_profit = Trade.get_total_closed_profit()
else:
# 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_in_trades = sum(trade.stake_amount for trade in open_trades)
used_stake = 0.0
@ -128,9 +139,9 @@ class Wallets:
if isinstance(balances[currency], dict):
self._wallets[currency] = Wallet(
currency,
balances[currency].get("free"),
balances[currency].get("used"),
balances[currency].get("total"),
balances[currency].get("free", 0),
balances[currency].get("used", 0),
balances[currency].get("total", 0),
)
# Remove currencies no longer in get_balances output
for currency in deepcopy(self._wallets):
@ -138,7 +149,7 @@ class Wallets:
del self._wallets[currency]
positions = self._exchange.fetch_positions()
self._positions = {}
_parsed_positions = {}
for position in positions:
symbol = position["symbol"]
if position["side"] is None or position["collateral"] == 0.0:
@ -146,14 +157,15 @@ class Wallets:
continue
size = self._exchange._contracts_to_amount(symbol, position["contracts"])
collateral = safe_value_fallback(position, "collateral", "initialMargin", 0.0)
leverage = position["leverage"]
self._positions[symbol] = PositionWallet(
leverage = position.get("leverage")
_parsed_positions[symbol] = PositionWallet(
symbol,
position=size,
leverage=leverage,
collateral=collateral,
side=position["side"],
)
self._positions = _parsed_positions
def update(self, require_update: bool = True) -> None:
"""

View File

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

View File

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

View File

@ -112,3 +112,12 @@ markdown_extensions:
custom_checkbox: true
- pymdownx.tilde
- 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"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
addopts = "--dist loadscope"
[tool.mypy]

View File

@ -7,11 +7,11 @@
-r docs/requirements-docs.txt
coveralls==4.0.1
ruff==0.5.4
mypy==1.11.0
pre-commit==3.7.1
pytest==8.3.1
pytest-asyncio==0.23.8
ruff==0.6.2
mypy==1.11.2
pre-commit==3.8.0
pytest==8.3.2
pytest-asyncio==0.24.0
pytest-cov==5.0.0
pytest-mock==3.14.0
pytest-random-order==1.1.1
@ -19,14 +19,14 @@ pytest-timeout==2.3.1
pytest-xdist==3.6.1
isort==5.13.2
# For datetime mocking
time-machine==2.14.2
time-machine==2.15.0
# Convert jupyter notebooks to markdown documents
nbconvert==7.16.4
# mypy types
types-cachetools==5.4.0.20240717
types-cachetools==5.5.0.20240820
types-filelock==3.2.7
types-requests==2.32.0.20240712
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
# 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.4.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==0.29.1
stable_baselines3==2.3.2
sb3_contrib>=2.2.1
# 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