Merge branch 'develop' into feature/fetch-public-trades

This commit is contained in:
Matthias 2024-04-05 06:35:35 +02:00
commit 63ac183e91
99 changed files with 5849 additions and 1672 deletions

View File

@ -0,0 +1,47 @@
name: Binance Leverage tiers update
on:
schedule:
- cron: "0 3 * * 4"
# on demand
workflow_dispatch:
permissions:
contents: read
jobs:
auto-update:
runs-on: ubuntu-latest
environment:
name: develop
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install ccxt
run: pip install ccxt
- name: Run leverage tier update
env:
CI_WEB_PROXY: ${{ secrets.CI_WEB_PROXY }}
FREQTRADE__EXCHANGE__KEY: ${{ secrets.BINANCE_EXCHANGE_KEY }}
FREQTRADE__EXCHANGE__SECRET: ${{ secrets.BINANCE_EXCHANGE_SECRET }}
run: python build_helpers/binance_update_lev_tiers.py
- uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.REPO_SCOPED_TOKEN }}
add-paths: freqtrade/exchange/binance_leverage_tiers.json
labels: |
Tech maintenance
Dependencies
branch: update/binance-leverage-tiers
title: Update Binance Leverage Tiers
commit-message: "chore: update pre-commit hooks"
committer: Freqtrade Bot <noreply@github.com>
body: Update binance leverage tiers.
delete-branch: true

View File

@ -11,7 +11,7 @@ on:
types: [published]
pull_request:
schedule:
- cron: '0 5 * * 4'
- cron: '0 3 * * 4'
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}"
@ -19,7 +19,7 @@ concurrency:
permissions:
repository-projects: read
jobs:
build_linux:
build-linux:
runs-on: ${{ matrix.os }}
strategy:
@ -60,11 +60,16 @@ jobs:
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
pip install -r requirements-dev.txt
pip install -e ft_client/
pip install -e .
- name: Check for version alignment
run: |
python build_helpers/freqtrade_client_version_align.py
- name: Tests
run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
- name: Coveralls
if: (runner.os == 'Linux' && matrix.python-version == '3.10' && matrix.os == 'ubuntu-22.04')
@ -188,6 +193,7 @@ jobs:
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
pip install -r requirements-dev.txt
pip install -e ft_client/
pip install -e .
- name: Tests
@ -362,7 +368,7 @@ jobs:
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
build_linux_online:
build-linux-online:
# Run pytest with "live" checks
runs-on: ubuntu-22.04
steps:
@ -398,25 +404,26 @@ jobs:
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
pip install -r requirements-dev.txt
pip install -e ft_client/
pip install -e .
- name: Tests incl. ccxt compatibility tests
env:
CI_WEB_PROXY: http://152.67.78.211:13128
run: |
pytest --random-order --longrun --durations 20 -n auto --dist loadscope
pytest --random-order --longrun --durations 20 -n auto
# Notify only once - when CI completes (and after deploy) in case it's successfull
notify-complete:
needs: [
build_linux,
build-linux,
build-macos,
build-windows,
docs-check,
mypy-version-check,
pre-commit,
build_linux_online
build-linux-online
]
runs-on: ubuntu-22.04
# Discord notification can't handle schedule events
@ -443,7 +450,7 @@ jobs:
build:
name: "Build"
needs: [ build_linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
needs: [ build-linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
runs-on: ubuntu-22.04
steps:
@ -467,6 +474,19 @@ jobs:
dist
retention-days: 10
- name: Build Client distribution
run: |
pip install -U build
python -m build --sdist --wheel ft_client
- name: Upload artifacts 📦
uses: actions/upload-artifact@v4
with:
name: freqtrade-client-build
path: |
ft_client/dist
retention-days: 10
deploy-pypi:
name: "Deploy to PyPI"
needs: [ build ]
@ -484,8 +504,10 @@ jobs:
- name: Download artifact 📦
uses: actions/download-artifact@v4
with:
name: freqtrade-build
pattern: freqtrade*-build
path: dist
merge-multiple: true
- name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.8.14
@ -497,7 +519,7 @@ jobs:
deploy-docker:
needs: [ build_linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
needs: [ build-linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
runs-on: ubuntu-22.04
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'

View File

@ -9,9 +9,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v4
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKERHUB_REPOSITORY: freqtradeorg/freqtrade
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: freqtradeorg/freqtrade

View File

@ -1,7 +1,6 @@
name: Pre-commit auto-update
on:
# every day at midnight
schedule:
- cron: "0 3 * * 2"
# on demand
@ -14,32 +13,32 @@ jobs:
auto-update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install pre-commit
run: pip install pre-commit
- name: Install pre-commit
run: pip install pre-commit
- name: Run auto-update
run: pre-commit autoupdate
- name: Run auto-update
run: pre-commit autoupdate
- name: Run pre-commit
run: pre-commit run --all-files
- name: Run pre-commit
run: pre-commit run --all-files
- uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.REPO_SCOPED_TOKEN }}
add-paths: .pre-commit-config.yaml
labels: |
Tech maintenance
Dependencies
branch: update/pre-commit-hooks
title: Update pre-commit hooks
commit-message: "chore: update pre-commit hooks"
committer: Freqtrade Bot <noreply@github.com>
body: Update versions of pre-commit hooks to latest version.
delete-branch: true
- uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.REPO_SCOPED_TOKEN }}
add-paths: .pre-commit-config.yaml
labels: |
Tech maintenance
Dependencies
branch: update/pre-commit-hooks
title: Update pre-commit hooks
commit-message: "chore: update pre-commit hooks"
committer: Freqtrade Bot <noreply@github.com>
body: Update versions of pre-commit hooks to latest version.
delete-branch: true

View File

@ -18,8 +18,8 @@ repos:
- types-filelock==3.2.7
- types-requests==2.31.0.20240311
- types-tabulate==0.9.0.20240106
- types-python-dateutil==2.8.19.20240311
- SQLAlchemy==2.0.28
- types-python-dateutil==2.9.0.20240316
- SQLAlchemy==2.0.29
# 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.3.2'
rev: 'v0.3.5'
hooks:
- id: ruff

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
import json
import os
from pathlib import Path
import ccxt
key = os.environ.get('FREQTRADE__EXCHANGE__KEY')
secret = os.environ.get('FREQTRADE__EXCHANGE__SECRET')
proxy = os.environ.get('CI_WEB_PROXY')
exchange = ccxt.binance({
'apiKey': key,
'secret': secret,
'httpsProxy': proxy,
'options': {'defaultType': 'swap'}
})
_ = exchange.load_markets()
lev_tiers = exchange.fetch_leverage_tiers()
# Assumes this is running in the root of the repository.
file = Path('freqtrade/exchange/binance_leverage_tiers.json')
json.dump(dict(sorted(lev_tiers.items())), file.open('w'), indent=2)

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
from freqtrade_client import __version__ as client_version
from freqtrade import __version__ as ft_version
def main():
if ft_version != client_version:
print(f"Versions do not match: \n"
f"ft: {ft_version} \n"
f"client: {client_version}")
exit(1)
print(f"Versions match: ft: {ft_version}, client: {client_version}")
exit(0)
if __name__ == '__main__':
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -252,34 +252,34 @@ The most important in the backtesting is to understand the result.
A backtesting result will look like that:
```
========================================================= BACKTESTING REPORT =========================================================
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% |
|:---------|--------:|---------------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:|
| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 |
| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 |
| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 |
| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 |
| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 |
| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 |
| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 |
| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 |
| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 |
| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 |
| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 |
| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 |
| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 |
| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 |
| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 |
| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 |
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
====================================================== LEFT OPEN TRADES REPORT ======================================================
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
================================================ BACKTESTING REPORT =================================================
| Pair | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% |
|:---------|--------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:|
| ADA/BTC | 35 | -0.11 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 |
| ARK/BTC | 11 | -0.41 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 |
| BTS/BTC | 32 | 0.31 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 |
| DASH/BTC | 13 | -0.08 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 |
| ENG/BTC | 18 | 1.36 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 |
| EOS/BTC | 36 | 0.08 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 |
| ETC/BTC | 26 | 0.37 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 |
| ETH/BTC | 33 | 0.30 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 |
| IOTA/BTC | 32 | 0.03 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 |
| LSK/BTC | 15 | 1.75 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 |
| LTC/BTC | 32 | -0.04 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 |
| NANO/BTC | 17 | 1.26 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 |
| NEO/BTC | 23 | 0.82 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 |
| REQ/BTC | 9 | 1.17 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 |
| XLM/BTC | 16 | 1.22 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 |
| XMR/BTC | 23 | -0.18 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 |
| XRP/BTC | 35 | 0.66 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
| ZEC/BTC | 22 | -0.46 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
| TOTAL | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
============================================= LEFT OPEN TRADES REPORT =============================================
| Pair | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|:---------|---------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
| ADA/BTC | 1 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
| LTC/BTC | 1 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
| TOTAL | 2 | 0.78 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
==================== EXIT REASON STATS ====================
| Exit Reason | Exits | Wins | Draws | Losses |
|:-------------------|--------:|------:|-------:|--------:|
@ -358,7 +358,7 @@ here:
The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has
earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
The column `Avg Profit %` shows the average profit for all trades made while the column `Cum Profit %` sums up all the profits/losses.
The column `Avg Profit %` shows the average profit for all trades made.
The column `Tot Profit %` shows instead the total profit % in relation to the starting balance.
In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`.
@ -464,7 +464,7 @@ It contains some useful key metrics about performance of your strategy on backte
- `Profit factor`: profit / loss.
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`.
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Tot Profit %`.
- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade.
- `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
@ -629,11 +629,11 @@ There will be an additional table comparing win/losses of the different strategi
Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
```
=========================================================== STRATEGY SUMMARY ===========================================================================
| Strategy | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % |
|:------------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:|
| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 |
| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 |
================================================== STRATEGY SUMMARY ===================================================================
| Strategy | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % |
|:------------|---------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:|
| Strategy1 | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 |
| Strategy2 | 1487 | -0.13 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 |
```
## Next step

View File

@ -33,7 +33,6 @@ For spot pairs, naming will be `base/quote` (e.g. `ETH/USDT`).
For futures pairs, naming will be `base/quote:settle` (e.g. `ETH/USDT:USDT`).
## Bot execution logic
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
@ -50,10 +49,12 @@ By default, the bot loop runs every few seconds (`internals.process_throttle_sec
* Call `populate_indicators()`
* Call `populate_entry_trend()`
* Call `populate_exit_trend()`
* Check timeouts for open orders.
* Calls `check_entry_timeout()` strategy callback for open entry orders.
* Calls `check_exit_timeout()` strategy callback for open exit orders.
* Calls `adjust_entry_price()` strategy callback for open entry orders.
* Update trades open order state from exchange.
* Call `order_filled()` strategy callback for filled orders.
* Check timeouts for open orders.
* Calls `check_entry_timeout()` strategy callback for open entry orders.
* Calls `check_exit_timeout()` strategy callback for open exit orders.
* Calls `adjust_entry_price()` strategy callback for open entry orders.
* Verifies existing positions and eventually places exit orders.
* Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`.
* Determine exit-price based on `exit_pricing` configuration setting or by using the `custom_exit_price()` callback.
@ -86,8 +87,10 @@ This loop will be repeated again and again until the bot is stopped.
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
* Determine stake size by calling the `custom_stake_amount()` callback.
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
* Call `order_filled()` strategy callback for filled entry orders.
* Call `custom_stoploss()` and `custom_exit()` to find custom exit points.
* For exits based on exit-signal, custom-exit and partial exits: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
* Call `order_filled()` strategy callback for filled exit orders.
* Generate backtest report output
!!! Note

View File

@ -49,6 +49,13 @@ FREQTRADE__EXCHANGE__SECRET=<yourExchangeSecret>
!!! Note
Environment variables detected are logged at startup - so if you can't find why a value is not what you think it should be based on the configuration, make sure it's not loaded from an environment variable.
!!! Tip "Validate combined result"
You can use the [show-config subcommand](utils.md#show-config) to see the final, combined configuration.
??? Warning "Loading sequence"
Environment variables are loaded after the initial configuration. As such, you cannot provide the path to the configuration through environment variables. Please use `--config path/to/config.json` for that.
This also applies to user_dir to some degree. while the user directory can be set through environment variables - the configuration will **not** be loaded from that location.
### Multiple configuration files
Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
@ -56,6 +63,9 @@ Multiple configuration files can be specified and used by the bot or the bot can
You can specify additional configuration files in `add_config_files`. Files specified in this parameter will be loaded and merged with the initial config file. The files are resolved relative to the initial configuration file.
This is similar to using multiple `--config` parameters, but simpler in usage as you don't have to specify all files for all commands.
!!! Tip "Validate combined result"
You can use the [show-config subcommand](utils.md#show-config) to see the final, combined configuration.
!!! Tip "Use multiple configuration files to keep secrets secret"
You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself.
The 2nd file should only specify what you intend to override.

View File

@ -129,6 +129,8 @@ Below is an outline of exception inheritance hierarchy:
+ FreqtradeException
|
+---+ OperationalException
| |
| +---+ ConfigurationError
|
+---+ DependencyException
| |
@ -376,7 +378,7 @@ from pathlib import Path
exchange = ccxt.binance({
'apiKey': '<apikey>',
'secret': '<secret>'
'secret': '<secret>',
'options': {'defaultType': 'swap'}
})
_ = exchange.load_markets()

View File

@ -32,6 +32,9 @@ FreqAI is configured through the typical [Freqtrade config file](configuration.m
A full example config is available in `config_examples/config_freqai.example.json`.
!!! Note
The `identifier` is commonly overlooked by newcomers, however, this value plays an important role in your configuration. This value is a unique ID that you choose to describe one of your runs. Keeping it the same allows you to maintain crash resilience as well as faster backtesting. As soon as you want to try a new run (new features, new model, etc.), you should change this value (or delete the `user_data/models/unique-id` folder. More details available in the [parameter table](freqai-parameter-table.md#feature-parameters).
## Building a FreqAI strategy
The FreqAI strategy requires including the following lines of code in the standard [Freqtrade strategy](strategy-customization.md):

View File

@ -81,12 +81,14 @@ Filtering instances (not the first position in the list) will not apply any cach
"number_assets": 20,
"sort_key": "quoteVolume",
"min_value": 0,
"max_value": 8000000,
"refresh_period": 1800
}
],
```
You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange.
In addition to that, you can also define a maximum volume with `max_value` - which will filter out pairs with a volume higher than the specified value in the specified timerange.
##### VolumePairList Advanced mode

View File

@ -23,6 +23,7 @@ It also supports the lookahead-analysis of freqai strategies.
- `--max-open-trades` is forced to be at least equal to the number of pairs.
- `--dry-run-wallet` is forced to be basically infinite (1 billion).
- `--stake-amount` is forced to be a static 10000 (10k).
- `--enable-protections` is forced to be off.
Those are set to avoid users accidentally generating false positives.
@ -40,7 +41,6 @@ usage: freqtrade lookahead-analysis [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--max-open-trades INT]
[--stake-amount STAKE_AMOUNT]
[--fee FLOAT] [-p PAIRS [PAIRS ...]]
[--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET]
[--timeframe-detail TIMEFRAME_DETAIL]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]

View File

@ -1,6 +1,6 @@
markdown==3.5.2
markdown==3.6
mkdocs==1.5.3
mkdocs-material==9.5.13
mkdocs-material==9.5.16
mdx_truly_sane_lists==1.3
pymdown-extensions==10.7.1
jinja2==3.1.3

View File

@ -95,11 +95,13 @@ Make sure that the following 2 lines are available in your docker-compose file:
### Consuming the API
You can consume the API by using the script `scripts/rest_client.py`.
The client script only requires the `requests` module, so Freqtrade does not need to be installed on the system.
You can consume the API by using `freqtrade-client` (also available as `scripts/rest_client.py`).
This command can be installed independent of the bot by using `pip install freqtrade-client`.
This module is designed to be lightweight, and only depends on the `requests` and `python-rapidjson` modules, skipping all heavy dependencies freqtrade otherwise needs.
``` bash
python3 scripts/rest_client.py <command> [optional parameters]
freqtrade-client <command> [optional parameters]
```
By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour.
@ -120,9 +122,27 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
```
``` bash
python3 scripts/rest_client.py --config rest_config.json <command> [optional parameters]
freqtrade-client --config rest_config.json <command> [optional parameters]
```
??? Note "Programmatic use"
The `freqtrade-client` package (installable independent of freqtrade) can be used in your own scripts to interact with the freqtrade API.
to do so, please use the following:
``` python
from freqtrade_client import FtRestClient
client = FtRestClient(server_url, username, password)
# Get the status of the bot
ping = client.ping()
print(ping)
# ...
```
For a full list of available commands, please refer to the list below.
### Available endpoints
| Command | Description |
@ -146,6 +166,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `mix_tags [pair]` | Shows profit statistics for each combinations of enter tag + exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.
| `locks` | Displays currently locked pairs.
| `delete_lock <lock_id>` | Deletes (disables) the lock by id.
| `locks add <pair>, <until>, [side], [reason]` | Locks a pair until "until". (Until will be rounded up to the nearest timeframe).
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
| `forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
@ -176,7 +197,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
Possible commands can be listed from the rest-client script using the `help` command.
``` bash
python3 scripts/rest_client.py help
freqtrade-client help
```
``` output

View File

@ -19,6 +19,7 @@ Currently available callbacks:
* [`adjust_trade_position()`](#adjust-trade-position)
* [`adjust_entry_price()`](#adjust-entry-price)
* [`leverage()`](#leverage-callback)
* [`order_filled()`](#order-filled-callback)
!!! Tip "Callback calling sequence"
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
@ -783,7 +784,7 @@ Additional entries are ignored once you have reached the maximum amount of extra
### Decrease position
The strategy is expected to return a negative stake_amount (in stake currency) for a partial exit.
Returning the full owned stake at that point (based on the current price) (`-(trade.amount / trade.leverage) * current_exit_rate`) results in a full exit.
Returning the full owned stake at that point (`-trade.stake_amount`) results in a full exit.
Returning a value more than the above (so remaining stake_amount would become negative) will result in the bot ignoring the signal.
!!! Note "About stake size"
@ -1022,3 +1023,33 @@ class AwesomeStrategy(IStrategy):
All profit calculations include leverage. Stoploss / ROI also include leverage in their calculation.
Defining a stoploss of 10% at 10x leverage would trigger the stoploss with a 1% move to the downside.
## Order filled Callback
The `order_filled()` callback may be used to perform specific actions based on the current trade state after an order is filled.
It will be called independent of the order type (entry, exit, stoploss or position adjustment).
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
class AwesomeStrategy(IStrategy):
def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None:
"""
Called right after an order fills.
Will be called for all order types (entry, exit, stoploss, position adjustment).
:param pair: Pair for trade
:param trade: trade object.
:param order: Order object.
: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.
"""
# Obtain pair dataframe (just to show how to access it)
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
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'])
return None
```

View File

@ -66,6 +66,53 @@ $ freqtrade new-config --config user_data/config_binance.json
? Do you want to enable Telegram? No
```
## Show config
Show configuration file (with sensitive values redacted by default).
Especially useful with [split configuration files](configuration.md#multiple-configuration-files) or [environment variables](configuration.md#environment-variables), where this command will show the merged configuration.
![Show config output](assets/show-config-output.png)
```
usage: freqtrade show-config [-h] [--userdir PATH] [-c PATH]
[--show-sensitive]
options:
-h, --help show this help message and exit
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
--show-sensitive Show secrets in the output.
```
``` output
Your combined configuration is:
{
"exit_pricing": {
"price_side": "other",
"use_order_book": true,
"order_book_top": 1
},
"stake_currency": "USDT",
"exchange": {
"name": "binance",
"key": "REDACTED",
"secret": "REDACTED",
"ccxt_config": {},
"ccxt_async_config": {},
}
// ...
}
```
!!! Warning "Sharing information provided by this command"
We try to remove all known sensitive information from the default output (without `--show-sensitive`).
Yet, please do double-check for sensitive values in your output to make sure you're not accidentally exposing some private info.
## Create new strategy
Creates a new strategy from a template similar to SampleStrategy.

View File

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

View File

@ -8,7 +8,7 @@ Note: Be careful with file-scoped imports in these subfiles.
"""
from freqtrade.commands.analyze_commands import start_analysis_entries_exits
from freqtrade.commands.arguments import Arguments
from freqtrade.commands.build_config_commands import start_new_config
from freqtrade.commands.build_config_commands import start_new_config, start_show_config
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades,
start_download_data, start_list_data)
from freqtrade.commands.db_commands import start_convert_db

View File

@ -4,7 +4,7 @@ from typing import Any, Dict
from freqtrade.configuration import setup_utils_configuration
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import ConfigurationError, OperationalException
logger = logging.getLogger(__name__)
@ -34,9 +34,9 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s
btfile = Path(config['exportfilename'])
signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl"
else:
raise OperationalException(f"{config['exportfilename']} does not exist.")
raise ConfigurationError(f"{config['exportfilename']} does not exist.")
else:
raise OperationalException('exportfilename not in config.')
raise ConfigurationError('exportfilename not in config.')
if (not Path(signals_file).exists()):
raise OperationalException(

View File

@ -62,6 +62,7 @@ ARGS_TEST_PAIRLIST = ["user_data_dir", "verbosity", "config", "quote_currencies"
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
ARGS_BUILD_CONFIG = ["config"]
ARGS_SHOW_CONFIG = ["user_data_dir", "config", "show_sensitive"]
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
@ -209,9 +210,9 @@ class Arguments:
start_list_strategies, start_list_timeframes,
start_lookahead_analysis, start_new_config,
start_new_strategy, start_plot_dataframe, start_plot_profit,
start_recursive_analysis, start_show_trades,
start_strategy_update, start_test_pairlist, start_trading,
start_webserver)
start_recursive_analysis, start_show_config,
start_show_trades, start_strategy_update,
start_test_pairlist, start_trading, start_webserver)
subparsers = self.parser.add_subparsers(dest='command',
# Use custom message when no subhandler is added
@ -244,6 +245,14 @@ class Arguments:
build_config_cmd.set_defaults(func=start_new_config)
self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd)
# add show-config subcommand
show_config_cmd = subparsers.add_parser(
'show-config',
help="Show resolved config",
)
show_config_cmd.set_defaults(func=start_show_config)
self._build_args(optionlist=ARGS_SHOW_CONFIG, parser=show_config_cmd)
# add new-strategy subcommand
build_strategy_cmd = subparsers.add_parser(
'new-strategy',

View File

@ -5,9 +5,12 @@ from typing import Any, Dict, List
from questionary import Separator, prompt
from freqtrade.configuration import sanitize_config
from freqtrade.configuration.config_setup import setup_utils_configuration
from freqtrade.configuration.detect_environment import running_in_docker
from freqtrade.configuration.directory_operations import chown_user_directory
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges
from freqtrade.util import render_template
@ -264,3 +267,19 @@ def start_new_config(args: Dict[str, Any]) -> None:
"Please delete it or use a different configuration file name.")
selections = ask_user_config()
deploy_new_config(config_path, selections)
def start_show_config(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE, set_dry=False)
# TODO: Sanitize from sensitive info before printing
print("Your combined configuration is:")
config_sanitized = sanitize_config(
config['original_config'],
show_sensitive=args.get('show_sensitive', False)
)
from rich import print_json
print_json(data=config_sanitized)

View File

@ -716,4 +716,10 @@ AVAILABLE_CLI_OPTIONS = {
help='Specify startup candles to be checked (`199`, `499`, `999`, `1999`).',
nargs='+',
),
"show_sensitive": Arg(
'--show-sensitive',
help='Show secrets in the output.',
action='store_true',
default=False,
),
}

View File

@ -9,7 +9,7 @@ from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_forma
convert_trades_to_ohlcv)
from freqtrade.data.history import download_data_main
from freqtrade.enums import CandleType, RunMode, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import ConfigurationError
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
from freqtrade.resolvers import ExchangeResolver
@ -21,11 +21,11 @@ logger = logging.getLogger(__name__)
def _check_data_config_download_sanity(config: Config) -> None:
if 'days' in config and 'timerange' in config:
raise OperationalException("--days and --timerange are mutually exclusive. "
"You can only specify one or the other.")
raise ConfigurationError("--days and --timerange are mutually exclusive. "
"You can only specify one or the other.")
if 'pairs' not in config:
raise OperationalException(
raise ConfigurationError(
"Downloading data requires a list of pairs. "
"Please check the documentation on how to configure this.")

View File

@ -9,7 +9,7 @@ from freqtrade.configuration import setup_utils_configuration
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
from freqtrade.constants import USERPATH_STRATEGIES
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import ConfigurationError, OperationalException
from freqtrade.util import render_template, render_template_with_fallback
@ -89,7 +89,7 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
deploy_new_strategy(args['strategy'], new_path, args['template'])
else:
raise OperationalException("`new-strategy` requires --strategy to be set.")
raise ConfigurationError("`new-strategy` requires --strategy to be set.")
def clean_ui_subdir(directory: Path):

View File

@ -10,7 +10,7 @@ from tabulate import tabulate
from freqtrade.configuration import setup_utils_configuration
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import ConfigurationError, OperationalException
from freqtrade.exchange import list_available_exchanges, market_is_active
from freqtrade.misc import parse_db_uri_for_logging, plural
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
@ -246,7 +246,7 @@ def start_show_trades(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if 'db_url' not in config:
raise OperationalException("--db-url is required for this command.")
raise ConfigurationError("--db-url is required for this command.")
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
init_db(config['db_url'])

View File

@ -4,7 +4,7 @@ from typing import Any, Dict
from freqtrade import constants
from freqtrade.configuration import setup_utils_configuration
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import ConfigurationError, OperationalException
from freqtrade.util import fmt_coin
@ -31,7 +31,7 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
and config['stake_amount'] > wallet_size):
wallet = fmt_coin(wallet_size, config['stake_currency'])
stake = fmt_coin(config['stake_amount'], config['stake_currency'])
raise OperationalException(
raise ConfigurationError(
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
)

View File

@ -2,12 +2,12 @@ from typing import Any, Dict
from freqtrade.configuration import setup_utils_configuration
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import ConfigurationError
def validate_plot_args(args: Dict[str, Any]) -> None:
if not args.get('datadir') and not args.get('config'):
raise OperationalException(
raise ConfigurationError(
"You need to specify either `--datadir` or `--config` "
"for plot-profit and plot-dataframe.")

View File

@ -23,11 +23,6 @@ def start_trading(args: Dict[str, Any]) -> int:
signal.signal(signal.SIGTERM, term_handler)
worker = Worker(args)
worker.run()
except Exception as e:
logger.error(str(e))
logger.exception("Fatal exception!")
except (KeyboardInterrupt):
logger.info('SIGINT received, aborting ...')
finally:
if worker:
logger.info("worker found ... calling exit")

View File

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

View File

@ -0,0 +1,36 @@
from copy import deepcopy
from freqtrade.constants import Config
def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
"""
Remove sensitive information from the config.
:param config: Configuration
:param show_sensitive: Show sensitive information
:return: Configuration
"""
if show_sensitive:
return config
keys_to_remove = [
"exchange.key",
"exchange.secret",
"exchange.password",
"exchange.uid",
"telegram.token",
"telegram.chat_id",
"discord.webhook_url",
"api_server.password",
]
config = deepcopy(config)
for key in keys_to_remove:
if '.' in key:
nested_keys = key.split('.')
nested_config = config
for nested_key in nested_keys[:-1]:
nested_config = nested_config.get(nested_key, {})
nested_config[nested_keys[-1]] = 'REDACTED'
else:
config[key] = 'REDACTED'
return config

View File

@ -10,7 +10,8 @@ from .configuration import Configuration
logger = logging.getLogger(__name__)
def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]:
def setup_utils_configuration(
args: Dict[str, Any], method: RunMode, *, set_dry: bool = True) -> Dict[str, Any]:
"""
Prepare the configuration for utils subcommands
:param args: Cli args from Arguments()
@ -21,7 +22,8 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str
config = configuration.get_config()
# Ensure these modes are using Dry-run
config['dry_run'] = True
if set_dry:
config['dry_run'] = True
validate_config_consistency(config, preliminary=True)
return config

View File

@ -9,7 +9,7 @@ from jsonschema.exceptions import ValidationError, best_match
from freqtrade import constants
from freqtrade.configuration.deprecated_settings import process_deprecated_setting
from freqtrade.enums import RunMode, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import ConfigurationError
logger = logging.getLogger(__name__)
@ -73,7 +73,7 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal
Should be ran after loading both configuration and strategy,
since strategies can set certain configuration settings too.
:param conf: Config in JSON format
:return: Returns None if everything is ok, otherwise throw an OperationalException
:return: Returns None if everything is ok, otherwise throw an ConfigurationError
"""
# validating trailing stoploss
@ -99,12 +99,12 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal
def _validate_unlimited_amount(conf: Dict[str, Any]) -> None:
"""
If edge is disabled, either max_open_trades or stake_amount need to be set.
:raise: OperationalException if config validation failed
:raise: ConfigurationError if config validation failed
"""
if (not conf.get('edge', {}).get('enabled')
and conf.get('max_open_trades') == float('inf')
and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT):
raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.")
raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.")
def _validate_price_config(conf: Dict[str, Any]) -> None:
@ -114,18 +114,18 @@ def _validate_price_config(conf: Dict[str, Any]) -> None:
# TODO: The below could be an enforced setting when using market orders
if (conf.get('order_types', {}).get('entry') == 'market'
and conf.get('entry_pricing', {}).get('price_side') not in ('ask', 'other')):
raise OperationalException(
raise ConfigurationError(
'Market entry orders require entry_pricing.price_side = "other".')
if (conf.get('order_types', {}).get('exit') == 'market'
and conf.get('exit_pricing', {}).get('price_side') not in ('bid', 'other')):
raise OperationalException('Market exit orders require exit_pricing.price_side = "other".')
raise ConfigurationError('Market exit orders require exit_pricing.price_side = "other".')
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
if conf.get('stoploss') == 0.0:
raise OperationalException(
raise ConfigurationError(
'The config stoploss needs to be different from 0 to avoid problems with sell orders.'
)
# Skip if trailing stoploss is not activated
@ -138,17 +138,17 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
if tsl_only_offset:
if tsl_positive == 0.0:
raise OperationalException(
raise ConfigurationError(
'The config trailing_only_offset_is_reached needs '
'trailing_stop_positive_offset to be more than 0 in your config.')
if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive:
raise OperationalException(
raise ConfigurationError(
'The config trailing_stop_positive_offset needs '
'to be greater than trailing_stop_positive in your config.')
# Fetch again without default
if 'trailing_stop_positive' in conf and float(conf['trailing_stop_positive']) == 0.0:
raise OperationalException(
raise ConfigurationError(
'The config trailing_stop_positive needs to be different from 0 '
'to avoid problems with sell orders.'
)
@ -163,7 +163,7 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
return
if not conf.get('use_exit_signal', True):
raise OperationalException(
raise ConfigurationError(
"Edge requires `use_exit_signal` to be True, otherwise no sells will happen."
)
@ -179,7 +179,7 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]):
if (isinstance(pl, dict) and pl.get('method') == 'StaticPairList'
and not conf.get('exchange', {}).get('pair_whitelist')):
raise OperationalException("StaticPairList requires pair_whitelist to be set.")
raise ConfigurationError("StaticPairList requires pair_whitelist to be set.")
def _validate_protections(conf: Dict[str, Any]) -> None:
@ -189,13 +189,13 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
for prot in conf.get('protections', []):
if ('stop_duration' in prot and 'stop_duration_candles' in prot):
raise OperationalException(
raise ConfigurationError(
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
f"Please fix the protection {prot.get('method')}"
)
if ('lookback_period' in prot and 'lookback_period_candles' in prot):
raise OperationalException(
raise ConfigurationError(
"Protections must specify either `lookback_period` or `lookback_period_candles`.\n"
f"Please fix the protection {prot.get('method')}"
)
@ -207,7 +207,7 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
ob_max = ask_strategy.get('order_book_max')
if ob_min is not None and ob_max is not None and ask_strategy.get('use_order_book'):
if ob_min != ob_max:
raise OperationalException(
raise ConfigurationError(
"Using order_book_max != order_book_min in exit_pricing is no longer supported."
"Please pick one value and use `order_book_top` in the future."
)
@ -235,7 +235,7 @@ def _validate_time_in_force(conf: Dict[str, Any]) -> None:
time_in_force = conf.get('order_time_in_force', {})
if 'buy' in time_in_force or 'sell' in time_in_force:
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
raise OperationalException(
raise ConfigurationError(
"Please migrate your time_in_force settings to use 'entry' and 'exit'.")
else:
logger.warning(
@ -256,7 +256,7 @@ def _validate_order_types(conf: Dict[str, Any]) -> None:
'forcesell', 'emergencyexit', 'forceexit', 'forceentry']
if any(x in order_types for x in old_order_types):
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
raise OperationalException(
raise ConfigurationError(
"Please migrate your order_types settings to use the new wording.")
else:
logger.warning(
@ -281,7 +281,7 @@ def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None:
unfilledtimeout = conf.get('unfilledtimeout', {})
if any(x in unfilledtimeout for x in ['buy', 'sell']):
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
raise OperationalException(
raise ConfigurationError(
"Please migrate your unfilledtimeout settings to use the new wording.")
else:
@ -301,7 +301,7 @@ def _validate_pricing_rules(conf: Dict[str, Any]) -> None:
if conf.get('ask_strategy') or conf.get('bid_strategy'):
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
raise OperationalException(
raise ConfigurationError(
"Please migrate your pricing settings to use the new wording.")
else:
@ -332,7 +332,7 @@ def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None:
freqai_enabled = conf.get('freqai', {}).get('enabled', False)
analyze_per_epoch = conf.get('analyze_per_epoch', False)
if analyze_per_epoch and freqai_enabled:
raise OperationalException(
raise ConfigurationError(
'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.')
@ -351,7 +351,7 @@ def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool)
if tf_s < main_tf_s:
offending_lines.append(tf)
if offending_lines:
raise OperationalException(
raise ConfigurationError(
f"Main timeframe of {main_tf} must be smaller or equal to FreqAI "
f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}")
@ -369,17 +369,17 @@ def _validate_freqai_backtest(conf: Dict[str, Any]) -> None:
timerange = conf.get('timerange')
freqai_backtest_live_models = conf.get('freqai_backtest_live_models', False)
if freqai_backtest_live_models and freqai_enabled and timerange:
raise OperationalException(
raise ConfigurationError(
'Using timerange parameter is not supported with '
'--freqai-backtest-live-models parameter.')
if freqai_backtest_live_models and not freqai_enabled:
raise OperationalException(
raise ConfigurationError(
'Using --freqai-backtest-live-models parameter is only '
'supported with a FreqAI strategy.')
if freqai_enabled and not freqai_backtest_live_models and not timerange:
raise OperationalException(
raise ConfigurationError(
'Please pass --timerange if you intend to use FreqAI for backtesting.')
@ -387,12 +387,12 @@ def _validate_consumers(conf: Dict[str, Any]) -> None:
emc_conf = conf.get('external_message_consumer', {})
if emc_conf.get('enabled', False):
if len(emc_conf.get('producers', [])) < 1:
raise OperationalException("You must specify at least 1 Producer to connect to.")
raise ConfigurationError("You must specify at least 1 Producer to connect to.")
producer_names = [p['name'] for p in emc_conf.get('producers', [])]
duplicates = [item for item, count in Counter(producer_names).items() if count > 1]
if duplicates:
raise OperationalException(
raise ConfigurationError(
f"Producer names must be unique. Duplicate: {', '.join(duplicates)}")
if conf.get('process_only_new_candles', True):
# Warning here or require it?

View File

@ -200,6 +200,12 @@ class Configuration:
config['exportfilename'] = (config['user_data_dir']
/ 'backtest_results')
if self.args.get('show_sensitive'):
logger.warning(
"Sensitive information will be shown in the upcomming output. "
"Please make sure to never share this output without redacting "
"the information yourself.")
def _process_optimize_options(self, config: Config) -> None:
# This will override the strategy configuration

View File

@ -6,7 +6,7 @@ import logging
from typing import Optional
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import ConfigurationError, OperationalException
logger = logging.getLogger(__name__)
@ -41,7 +41,7 @@ def process_removed_setting(config: Config,
section1_config = config.get(section1, {})
if name1 in section1_config:
section_2 = f"{section2}.{name2}" if section2 else f"{name2}"
raise OperationalException(
raise ConfigurationError(
f"Setting `{section1}.{name1}` has been moved to `{section_2}. "
f"Please delete it from your configuration and use the `{section_2}` "
"setting instead."
@ -122,7 +122,7 @@ def process_temporary_deprecated_settings(config: Config) -> None:
None, 'ignore_roi_if_entry_signal')
if (config.get('edge', {}).get('enabled', False)
and 'capital_available_percentage' in config.get('edge', {})):
raise OperationalException(
raise ConfigurationError(
"DEPRECATED: "
"Using 'edge.capital_available_percentage' has been deprecated in favor of "
"'tradable_balance_ratio'. Please migrate your configuration to "
@ -131,7 +131,7 @@ def process_temporary_deprecated_settings(config: Config) -> None:
)
if 'ticker_interval' in config:
raise OperationalException(
raise ConfigurationError(
"DEPRECATED: 'ticker_interval' detected. "
"Please use 'timeframe' instead of 'ticker_interval."
)

View File

@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional
import rapidjson
from freqtrade.constants import MINIMAL_CONFIG, Config
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import ConfigurationError, OperationalException
from freqtrade.misc import deep_merge_dicts
@ -66,7 +66,7 @@ def load_config_file(path: str) -> Dict[str, Any]:
' Please create a config file or check whether it exists.') from None
except rapidjson.JSONDecodeError as e:
err_range = log_config_error_range(path, str(e))
raise OperationalException(
raise ConfigurationError(
f'{e}\n'
f'Please verify the following segment of your configuration:\n{err_range}'
if err_range else 'Please verify your configuration file for syntax errors.'
@ -83,7 +83,7 @@ def load_from_files(
"""
config: Config = {}
if level > 5:
raise OperationalException("Config loop detected.")
raise ConfigurationError("Config loop detected.")
if not files:
return deepcopy(MINIMAL_CONFIG)

View File

@ -9,7 +9,7 @@ from typing import Optional
from typing_extensions import Self
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import ConfigurationError
logger = logging.getLogger(__name__)
@ -156,7 +156,7 @@ class TimeRange:
else:
stop = int(stops)
if start > stop > 0:
raise OperationalException(
raise ConfigurationError(
f'Start date is after stop date for timerange "{text}"')
return cls(stype[0], stype[1], start, stop)
raise OperationalException(f'Incorrect syntax for timerange "{text}"')
raise ConfigurationError(f'Incorrect syntax for timerange "{text}"')

View File

@ -11,7 +11,7 @@ import numpy as np
import pandas as pd
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import ConfigurationError, OperationalException
from freqtrade.misc import file_dump_json, json_load
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
from freqtrade.persistence import LocalTrade, Trade, init_db
@ -106,7 +106,7 @@ def get_latest_hyperopt_file(
directory = Path(directory)
if predef_filename:
if Path(predef_filename).is_absolute():
raise OperationalException(
raise ConfigurationError(
"--hyperopt-filename expects only the filename, not an absolute path.")
return directory / predef_filename
return directory / get_latest_hyperopt_filename(directory)

View File

@ -12,6 +12,12 @@ class OperationalException(FreqtradeException):
"""
class ConfigurationError(OperationalException):
"""
Configuration error. Usually caused by invalid configuration.
"""
class DependencyException(FreqtradeException):
"""
Indicates that an assumed dependency is not met.

View File

@ -4,6 +4,7 @@ from freqtrade.exchange.common import remove_exchange_credentials, MAP_EXCHANGE_
from freqtrade.exchange.exchange import Exchange
# isort: on
from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bingx import Bingx
from freqtrade.exchange.bitmart import Bitmart
from freqtrade.exchange.bitpanda import Bitpanda
from freqtrade.exchange.bitvavo import Bitvavo

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
""" Bingx exchange subclass """
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Bingx(Exchange):
"""
Bingx exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
"""
_ft_has: Dict = {
"ohlcv_candle_limit": 1000,
}

View File

@ -28,9 +28,10 @@ from freqtrade.data.converter.orderflow import _calculate_ohlcv_candle_start_and
from freqtrade.data.converter.trade_converter import (trades_df_remove_duplicates,
trades_dict_to_list, trades_list_to_df)
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, RunMode, TradingMode
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError,
RetryableOrderError, TemporaryError)
from freqtrade.exceptions import (ConfigurationError, DDosProtection, ExchangeError,
InsufficientFundsError, InvalidOrderException,
OperationalException, PricingError, RetryableOrderError,
TemporaryError)
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_exchange_credentials,
retrier, retrier_async)
from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType,
@ -93,6 +94,8 @@ class Exchange:
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
# Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
"marketOrderRequiresPrice": False,
"exchange_has_overrides": {}, # Dictionary overriding ccxt's "has".
# Expected to be in the format {"fetchOHLCV": True} or {"fetchOHLCV": False}
}
_ft_has: Dict = {}
_ft_has_futures: Dict = {}
@ -547,7 +550,7 @@ class Exchange:
)
quote_currencies = self.get_quote_currencies()
if stake_currency not in quote_currencies:
raise OperationalException(
raise ConfigurationError(
f"{stake_currency} is not available as stake on {self.name}. "
f"Available currencies are: {', '.join(quote_currencies)}")
@ -615,7 +618,7 @@ class Exchange:
f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}")
if timeframe and (timeframe not in self.timeframes):
raise OperationalException(
raise ConfigurationError(
f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}")
if (
@ -623,7 +626,7 @@ class Exchange:
and self._config['runmode'] != RunMode.UTIL_EXCHANGE
and timeframe_to_minutes(timeframe) < 1
):
raise OperationalException("Timeframes < 1m are currently not supported by Freqtrade.")
raise ConfigurationError("Timeframes < 1m are currently not supported by Freqtrade.")
def validate_ordertypes(self, order_types: Dict) -> None:
"""
@ -631,7 +634,7 @@ class Exchange:
"""
if any(v == 'market' for k, v in order_types.items()):
if not self.exchange_has('createMarketOrder'):
raise OperationalException(
raise ConfigurationError(
f'Exchange {self.name} does not support market orders.')
self.validate_stop_ordertypes(order_types)
@ -641,7 +644,7 @@ class Exchange:
"""
if (order_types.get("stoploss_on_exchange")
and not self._ft_has.get("stoploss_on_exchange", False)):
raise OperationalException(
raise ConfigurationError(
f'On exchange stoploss is not supported for {self.name}.'
)
if self.trading_mode == TradingMode.FUTURES:
@ -651,17 +654,17 @@ class Exchange:
and 'stoploss_price_type' in order_types
and order_types['stoploss_price_type'] not in price_mapping
):
raise OperationalException(
raise ConfigurationError(
f'On exchange stoploss price type is not supported for {self.name}.'
)
def validate_pricing(self, pricing: Dict) -> None:
if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'):
raise OperationalException(f'Orderbook not available for {self.name}.')
raise ConfigurationError(f'Orderbook not available for {self.name}.')
if (not pricing.get('use_order_book', False) and (
not self.exchange_has('fetchTicker')
or not self._ft_has['tickers_have_price'])):
raise OperationalException(f'Ticker pricing not available for {self.name}.')
raise ConfigurationError(f'Ticker pricing not available for {self.name}.')
def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
"""
@ -669,7 +672,7 @@ class Exchange:
"""
if any(v.upper() not in self._ft_has["order_time_in_force"]
for k, v in order_time_in_force.items()):
raise OperationalException(
raise ConfigurationError(
f'Time in force policies are not supported for {self.name} yet.')
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
@ -691,12 +694,12 @@ class Exchange:
if required_candle_call_count > 5:
# Only allow 5 calls per pair to somewhat limit the impact
raise OperationalException(
raise ConfigurationError(
f"This strategy requires {startup_candles} candles to start, "
"which is more than 5x "
f"the amount of candles {self.name} provides for {timeframe}.")
elif required_candle_call_count > 1:
raise OperationalException(
raise ConfigurationError(
f"This strategy requires {startup_candles} candles to start, which is more than "
f"the amount of candles {self.name} provides for {timeframe}.")
if required_candle_call_count > 1:
@ -737,6 +740,8 @@ class Exchange:
:param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
:return: bool
"""
if endpoint in self._ft_has.get('exchange_has_overrides', {}):
return self._ft_has['exchange_has_overrides'][endpoint]
return endpoint in self._api.has and self._api.has[endpoint]
def get_precision_amount(self, pair: str) -> Optional[float]:
@ -3261,3 +3266,4 @@ class Exchange:
# describes the min amt for a tier, and the lowest tier will always go down to 0
else:
raise ExchangeError(f"Cannot get maintenance ratio using {self.name}")
raise ExchangeError(f"Cannot get maintenance ratio using {self.name}")

View File

@ -96,9 +96,7 @@ class Gate(Exchange):
return trades
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
if self.trading_mode == TradingMode.FUTURES:
return safe_value_fallback2(order, order, 'id_stop', 'id')
return order['id']
return safe_value_fallback2(order, order, 'id_stop', 'id')
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
order = self.fetch_order(
@ -106,17 +104,19 @@ class Gate(Exchange):
pair=pair,
params={'stop': True}
)
if self.trading_mode == TradingMode.FUTURES:
if order['status'] == 'closed':
# Places a real order - which we need to fetch explicitly.
new_orderid = order.get('info', {}).get('trade_id')
if new_orderid:
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
order1['id_stop'] = order1['id']
order1['id'] = order_id
order1['stopPrice'] = order.get('stopPrice')
if order.get('status', 'open') == 'closed':
# Places a real order - which we need to fetch explicitly.
val = 'trade_id' if self.trading_mode == TradingMode.FUTURES else 'fired_order_id'
return order1
if new_orderid := order.get('info', {}).get(val):
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
order1['id_stop'] = order1['id']
order1['id'] = order_id
order1['type'] = 'stoploss'
order1['stopPrice'] = order.get('stopPrice')
order1['status_stop'] = 'triggered'
return order1
return order
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:

View File

@ -152,7 +152,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
"""
assert isinstance(self.n_steps, int), "Either `n_steps` or `n_epochs` should be set."
n_batches = n_obs // self.batch_size
n_epochs = min(self.n_steps // n_batches, 1)
n_epochs = max(self.n_steps // n_batches, 1)
if n_epochs <= 10:
logger.warning(
f"Setting low n_epochs: {n_epochs}. "

View File

@ -37,7 +37,6 @@ from freqtrade.rpc.rpc_types import (ProfitLossStr, RPCCancelMsg, RPCEntryMsg, R
RPCExitMsg, RPCProtectionMsg)
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import FtPrecise
from freqtrade.util.migrations import migrate_binance_futures_names
from freqtrade.wallets import Wallets
@ -667,7 +666,7 @@ class FreqtradeBot(LoggingMixin):
# We should decrease our position
amount = self.exchange.amount_to_contract_precision(
trade.pair,
abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate))))
abs(float(stake_amount * trade.amount / trade.stake_amount)))
if amount == 0.0:
logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.")
@ -1945,6 +1944,9 @@ class FreqtradeBot(LoggingMixin):
def _update_trade_after_fill(self, trade: Trade, order: Order) -> Trade:
if order.status in constants.NON_OPEN_EXCHANGE_STATES:
strategy_safe_wrapper(
self.strategy.order_filled, default_retval=None)(
pair=trade.pair, trade=trade, order=order, current_time=datetime.now(timezone.utc))
# If a entry order was closed, force update on stoploss on exchange
if order.ft_order_side == trade.entry_side:
trade = self.cancel_stoploss_on_exchange(trade)

View File

@ -7,8 +7,6 @@ import logging
import sys
from typing import Any, List, Optional
from freqtrade.util.gc_setup import gc_set_threshold
# check min. python version
if sys.version_info < (3, 9): # pragma: no cover
@ -16,8 +14,10 @@ if sys.version_info < (3, 9): # pragma: no cover
from freqtrade import __version__
from freqtrade.commands import Arguments
from freqtrade.exceptions import FreqtradeException, OperationalException
from freqtrade.constants import DOCS_LINK
from freqtrade.exceptions import ConfigurationError, FreqtradeException, OperationalException
from freqtrade.loggers import setup_logging_pre
from freqtrade.util.gc_setup import gc_set_threshold
logger = logging.getLogger('freqtrade')
@ -56,6 +56,9 @@ def main(sysargv: Optional[List[str]] = None) -> None:
except KeyboardInterrupt:
logger.info('SIGINT received, aborting ...')
return_code = 0
except ConfigurationError as e:
logger.error(f"Configuration error: {e}\n"
f"Please make sure to review the documentation at {DOCS_LINK}.")
except FreqtradeException as e:
logger.error(str(e))
return_code = 2

View File

@ -121,14 +121,22 @@ class LookaheadAnalysisSubFunctions:
@staticmethod
def calculate_config_overrides(config: Config):
if config.get('enable_protections', False):
# if protections are used globally, they can produce false positives.
config['enable_protections'] = False
logger.info('Protections were enabled. '
'Disabling protections now '
'since they could otherwise produce false positives.')
if config['targeted_trade_amount'] < config['minimum_trade_amount']:
# this combo doesn't make any sense.
raise OperationalException(
"Targeted trade amount can't be smaller than minimum trade amount."
)
if len(config['pairs']) > config['max_open_trades']:
logger.info('Max_open_trades were less than amount of pairs. '
'Set max_open_trades to amount of pairs just to avoid false positives.')
if len(config['pairs']) > config.get('max_open_trades', 0):
logger.info('Max_open_trades were less than amount of pairs '
'or defined in the strategy. '
'Set max_open_trades to amount of pairs '
'just to avoid false positives.')
config['max_open_trades'] = len(config['pairs'])
min_dry_run_wallet = 1000000000

View File

@ -603,6 +603,11 @@ class Backtesting:
if order and self._get_order_filled(order.ft_price, row):
order.close_bt_order(current_date, trade)
self._run_funding_fees(trade, current_date, force=True)
strategy_safe_wrapper(
self.strategy.order_filled,
default_retval=None)(
pair=trade.pair, trade=trade, # type: ignore[arg-type]
order=order, current_time=current_date)
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount):
# trade is still open
@ -882,6 +887,9 @@ class Backtesting:
precision_amount = self.exchange.get_precision_amount(pair)
amount = amount_to_contract_precision(amount_p, precision_amount, self.precision_mode,
contract_size)
if not amount:
# No amount left after truncating to precision.
return trade
# Backcalculate actual stake amount.
stake_amount = amount * propose_rate / leverage

View File

@ -6,13 +6,12 @@ from freqtrade.optimize.optimize_reports.bt_output import (generate_edge_table,
show_sorted_pairlist,
text_table_add_metrics,
text_table_bt_results,
text_table_exit_reason,
text_table_periodic_breakdown,
text_table_strategy, text_table_tags)
from freqtrade.optimize.optimize_reports.bt_storage import (store_backtest_analysis_results,
store_backtest_stats)
from freqtrade.optimize.optimize_reports.optimize_reports import (
generate_all_periodic_breakdown_stats, generate_backtest_stats, generate_daily_stats,
generate_exit_reason_stats, generate_pair_metrics, generate_periodic_breakdown_stats,
generate_rejected_signals, generate_strategy_comparison, generate_strategy_stats,
generate_tag_metrics, generate_trade_signal_candles, generate_trading_stats)
generate_pair_metrics, generate_periodic_breakdown_stats, generate_rejected_signals,
generate_strategy_comparison, generate_strategy_stats, generate_tag_metrics,
generate_trade_signal_candles, generate_trading_stats)

View File

@ -16,7 +16,7 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]:
"""
Generate floatformat (goes in line with _generate_result_line())
"""
return ['s', 'd', '.2f', '.2f', f'.{decimals_per_coin(stake_currency)}f',
return ['s', 'd', '.2f', f'.{decimals_per_coin(stake_currency)}f',
'.2f', 'd', 's', 's']
@ -25,7 +25,7 @@ def _get_line_header(first_column: str, stake_currency: str,
"""
Generate header lines (goes in line with _generate_result_line())
"""
return [first_column, direction, 'Avg Profit %', 'Cum Profit %',
return [first_column, direction, 'Avg Profit %',
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
'Win Draw Loss Win%']
@ -51,7 +51,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
headers = _get_line_header('Pair', stake_currency)
floatfmt = _get_line_floatfmt(stake_currency)
output = [[
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
t['key'], t['trades'], t['profit_mean_pct'], t['profit_total_abs'],
t['profit_total_pct'], t['duration_avg'],
generate_wins_draws_losses(t['wins'], t['draws'], t['losses'])
] for t in pair_results]
@ -60,33 +60,6 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
def text_table_exit_reason(exit_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
"""
Generate small table outlining Backtest results
:param exit_reason_stats: Exit reason metrics
:param stake_currency: Stakecurrency used
:return: pretty printed table with tabulate as string
"""
headers = [
'Exit Reason',
'Exits',
'Win Draws Loss Win%',
'Avg Profit %',
'Cum Profit %',
f'Tot Profit {stake_currency}',
'Tot Profit %',
]
output = [[
t.get('exit_reason', t.get('sell_reason')), t['trades'],
generate_wins_draws_losses(t['wins'], t['draws'], t['losses']),
t['profit_mean_pct'], t['profit_sum_pct'],
fmt_coin(t['profit_total_abs'], stake_currency, False),
t['profit_total_pct'],
] for t in exit_reason_stats]
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
@ -94,21 +67,23 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
:param stake_currency: stake-currency - used to correctly name headers
:return: pretty printed table with tabulate as string
"""
fallback: str = ''
if (tag_type == "enter_tag"):
headers = _get_line_header("TAG", stake_currency)
else:
headers = _get_line_header("TAG", stake_currency, 'Exits')
headers = _get_line_header("Exit Reason", stake_currency, 'Exits')
fallback = 'exit_reason'
floatfmt = _get_line_floatfmt(stake_currency)
output = [
[
t['key'] if t['key'] is not None and len(
t['key']) > 0 else "OTHER",
t['key'] if t.get('key') is not None and len(
str(t['key'])) > 0 else t.get(fallback, "OTHER"),
t['trades'],
t['profit_mean_pct'],
t['profit_sum_pct'],
t['profit_total_abs'],
t['profit_total_pct'],
t['duration_avg'],
t.get('duration_avg'),
generate_wins_draws_losses(
t['wins'],
t['draws'],
@ -166,7 +141,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str:
for t, dd in zip(strategy_results, drawdown)]
output = [[
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
t['key'], t['trades'], t['profit_mean_pct'], t['profit_total_abs'],
t['profit_total_pct'], t['duration_avg'],
generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown]
for t, drawdown in zip(strategy_results, drawdown)]
@ -256,9 +231,9 @@ def text_table_add_metrics(strat_results: Dict) -> str:
*short_metrics,
('', ''), # Empty line to improve readability
('Best Pair', f"{strat_results['best_pair']['key']} "
f"{strat_results['best_pair']['profit_sum']:.2%}"),
f"{strat_results['best_pair']['profit_total']:.2%}"),
('Worst Pair', f"{strat_results['worst_pair']['key']} "
f"{strat_results['worst_pair']['profit_sum']:.2%}"),
f"{strat_results['worst_pair']['profit_total']:.2%}"),
('Best trade', f"{best_trade['pair']} {best_trade['profit_ratio']:.2%}"),
('Worst trade', f"{worst_trade['pair']} "
f"{worst_trade['profit_ratio']:.2%}"),
@ -319,17 +294,16 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
if (results.get('results_per_enter_tag') is not None):
table = text_table_tags("enter_tag", results['results_per_enter_tag'], stake_currency)
if (enter_tags := results.get('results_per_enter_tag')) is not None:
table = text_table_tags("enter_tag", enter_tags, stake_currency)
if isinstance(table, str) and len(table) > 0:
print(' ENTER TAG STATS '.center(len(table.splitlines()[0]), '='))
print(table)
exit_reasons = results.get('exit_reason_summary')
if exit_reasons:
table = text_table_exit_reason(exit_reason_stats=exit_reasons,
stake_currency=stake_currency)
if (exit_reasons := results.get('exit_reason_summary')) is not None:
table = text_table_tags("exit_tag", exit_reasons, stake_currency)
if isinstance(table, str) and len(table) > 0:
print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '='))
print(table)

View File

@ -6,7 +6,7 @@ from typing import Any, Dict, List, Tuple, Union
import numpy as np
from pandas import DataFrame, Series, concat, to_datetime
from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, IntOrInf
from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
calculate_expectancy, calculate_market_change,
calculate_max_drawdown, calculate_sharpe, calculate_sortino)
@ -71,7 +71,8 @@ def _generate_result_line(result: DataFrame, starting_balance: int, first_column
'key': first_column,
'trades': len(result),
'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0,
'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0,
'profit_mean_pct': round(result['profit_ratio'].mean() * 100.0, 2
) if len(result) > 0 else 0.0,
'profit_sum': profit_sum,
'profit_sum_pct': round(profit_sum * 100.0, 2),
'profit_total_abs': result['profit_abs'].sum(),
@ -154,42 +155,6 @@ def generate_tag_metrics(tag_type: str,
return []
def generate_exit_reason_stats(max_open_trades: IntOrInf, results: DataFrame) -> List[Dict]:
"""
Generate small table outlining Backtest results
:param max_open_trades: Max_open_trades parameter
:param results: Dataframe containing the backtest result for one strategy
:return: List of Dicts containing the metrics per Sell reason
"""
tabular_data = []
for reason, count in results['exit_reason'].value_counts().items():
result = results.loc[results['exit_reason'] == reason]
profit_mean = result['profit_ratio'].mean()
profit_sum = result['profit_ratio'].sum()
profit_total = profit_sum / max_open_trades
tabular_data.append(
{
'exit_reason': reason,
'trades': count,
'wins': len(result[result['profit_abs'] > 0]),
'draws': len(result[result['profit_abs'] == 0]),
'losses': len(result[result['profit_abs'] < 0]),
'winrate': len(result[result['profit_abs'] > 0]) / count if count else 0.0,
'profit_mean': profit_mean,
'profit_mean_pct': round(profit_mean * 100, 2),
'profit_sum': profit_sum,
'profit_sum_pct': round(profit_sum * 100, 2),
'profit_total_abs': result['profit_abs'].sum(),
'profit_total': profit_total,
'profit_total_pct': round(profit_total * 100, 2),
}
)
return tabular_data
def generate_strategy_comparison(bt_stats: Dict) -> List[Dict]:
"""
Generate summary per strategy
@ -383,9 +348,8 @@ def generate_strategy_stats(pairlist: List[str],
enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance,
results=results, skip_nan=False)
exit_reason_stats = generate_exit_reason_stats(max_open_trades=max_open_trades,
results=results)
exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=start_balance,
results=results, skip_nan=False)
left_open_results = generate_pair_metrics(
pairlist, stake_currency=stake_currency, starting_balance=start_balance,
results=results.loc[results['exit_reason'] == 'force_exit'], skip_nan=True)

View File

@ -41,6 +41,7 @@ class VolumePairList(IPairList):
self._number_pairs = self._pairlistconfig['number_assets']
self._sort_key: Literal['quoteVolume'] = self._pairlistconfig.get('sort_key', 'quoteVolume')
self._min_value = self._pairlistconfig.get('min_value', 0)
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)
@ -139,6 +140,12 @@ class VolumePairList(IPairList):
"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.",
},
**IPairList.refresh_period_parameter(),
"lookback_days": {
"type": "number",
@ -270,6 +277,9 @@ class VolumePairList(IPairList):
if self._min_value > 0:
filtered_tickers = [
v for v in filtered_tickers if v[self._sort_key] > self._min_value]
if self._max_value is not None:
filtered_tickers = [
v for v in filtered_tickers if v[self._sort_key] < self._max_value]
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key])

View File

@ -14,7 +14,7 @@ from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_resu
get_backtest_resultlist, load_and_merge_backtest_result,
update_backtest_metadata)
from freqtrade.enums import BacktestState
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exceptions import ConfigurationError, DependencyException, OperationalException
from freqtrade.exchange.common import remove_exchange_credentials
from freqtrade.misc import deep_merge_dicts, is_file_in_dir
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestMetadataUpdate,
@ -98,10 +98,12 @@ def __run_backtest_bg(btconfig: Config):
logger.info("Backtest finished.")
except ConfigurationError as e:
logger.error(f"Backtesting encountered a configuration Error: {e}")
except (Exception, OperationalException, DependencyException) as e:
logger.exception(f"Backtesting caused an error: {e}")
ApiBG.bt['bt_error'] = str(e)
pass
finally:
ApiBG.bgtask_running = False

View File

@ -1,7 +1,7 @@
from datetime import date, datetime
from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, RootModel, SerializeAsAny
from pydantic import AwareDatetime, BaseModel, RootModel, SerializeAsAny
from freqtrade.constants import IntOrInf
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
@ -378,6 +378,13 @@ class Locks(BaseModel):
locks: List[LockModel]
class LocksPayload(BaseModel):
pair: str
side: str = '*' # Default to both sides
until: AwareDatetime
reason: Optional[str] = None
class DeleteLockRequest(BaseModel):
pair: Optional[str] = None
lockid: Optional[int] = None

View File

@ -15,10 +15,10 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac
DeleteLockRequest, DeleteTrade, Entry,
ExchangeListResponse, Exit, ForceEnterPayload,
ForceEnterResponse, ForceExitPayload,
FreqAIModelListResponse, Health, Locks, Logs,
MixTag, OpenTradeSchema, PairHistory,
PerformanceEntry, Ping, PlotConfig, Profit,
ResultMsg, ShowConfig, Stats, StatusMsg,
FreqAIModelListResponse, Health, Locks,
LocksPayload, Logs, MixTag, OpenTradeSchema,
PairHistory, PerformanceEntry, Ping, PlotConfig,
Profit, ResultMsg, ShowConfig, Stats, StatusMsg,
StrategyListResponse, StrategyResponse, SysInfo,
Version, WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional
@ -255,6 +255,13 @@ def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_delete_lock(lockid=payload.lockid, pair=payload.pair)
@router.post('/locks', response_model=Locks, tags=['info', 'locks'])
def add_locks(payload: List[LocksPayload], rpc: RPC = Depends(get_rpc)):
for lock in payload:
rpc._rpc_add_lock(lock.pair, lock.until, lock.reason, lock.side)
return rpc._rpc_locks()
@router.get('/logs', response_model=Logs, tags=['info'])
def logs(limit: Optional[int] = None):
return RPC._rpc_get_logs(limit)

View File

@ -1104,6 +1104,16 @@ class RPC:
return self._rpc_locks()
def _rpc_add_lock(
self, pair: str, until: datetime, reason: Optional[str], side: str) -> PairLock:
lock = PairLocks.lock_pair(
pair=pair,
until=until,
reason=reason,
side=side,
)
return lock
def _rpc_whitelist(self) -> Dict:
""" Returns the currently active whitelist"""
res = {'method': self._freqtrade.pairlists.name_list,

View File

@ -356,9 +356,11 @@ class Telegram(RPCHandler):
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
message += f" ({msg['leverage']:.3g}x)"
message += "`\n"
message += f"*Open Rate:* `{fmt_coin(msg['open_rate'], msg['quote_currency'])}`\n"
message += f"*Open Rate:* `{round_value(msg['open_rate'], 8)} {msg['quote_currency']}`\n"
if msg['type'] == RPCMessageType.ENTRY and msg['current_rate']:
message += f"*Current Rate:* `{fmt_coin(msg['current_rate'], msg['quote_currency'])}`\n"
message += (
f"*Current Rate:* `{round_value(msg['current_rate'], 8)} {msg['quote_currency']}`\n"
)
profit_fiat_extra = self.__format_profit_fiat(msg, 'stake_amount') # type: ignore
total = fmt_coin(msg['stake_amount'], msg['quote_currency'])
@ -563,19 +565,19 @@ class Telegram(RPCHandler):
lines.append(f"*{wording} #{order_nr}:*")
if order_nr == 1:
lines.append(
f"*Amount:* {cur_entry_amount:.8g} "
f"*Amount:* {round_value(cur_entry_amount, 8)} "
f"({fmt_coin(order['cost'], quote_currency)})"
)
lines.append(f"*Average Price:* {cur_entry_average:.8g}")
lines.append(f"*Average Price:* {round_value(cur_entry_average, 8)}")
else:
# TODO: This calculation ignores fees.
price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
if is_open:
lines.append("({})".format(dt_humanize(order["order_filled_date"],
granularity=["day", "hour", "minute"])))
lines.append(f"*Amount:* {cur_entry_amount:.8g} "
lines.append(f"*Amount:* {round_value(cur_entry_amount, 8)} "
f"({fmt_coin(order['cost'], quote_currency)})")
lines.append(f"*Average {wording} Price:* {cur_entry_average:.8g} "
lines.append(f"*Average {wording} Price:* {round_value(cur_entry_average, 8)} "
f"({price_to_1st_entry:.2%} from 1st entry rate)")
lines.append(f"*Order Filled:* {order['order_filled_date']}")
@ -687,11 +689,11 @@ class Telegram(RPCHandler):
])
lines.extend([
"*Open Rate:* `{open_rate:.8g}`",
"*Close Rate:* `{close_rate:.8g}`" if r['close_rate'] else "",
f"*Open Rate:* `{round_value(r['open_rate'], 8)}`",
f"*Close Rate:* `{round_value(r['close_rate'], 8)}`" if r['close_rate'] else "",
"*Open Date:* `{open_date}`",
"*Close Date:* `{close_date}`" if r['close_date'] else "",
" \n*Current Rate:* `{current_rate:.8g}`" if r['is_open'] else "",
f" \n*Current Rate:* `{round_value(r['current_rate'], 8)}`" if r['is_open'] else "",
("*Unrealized Profit:* " if r['is_open'] else "*Close Profit: *")
+ "`{profit_ratio:.2%}` `({profit_abs_r})`",
])
@ -712,9 +714,9 @@ class Telegram(RPCHandler):
"`({initial_stop_loss_ratio:.2%})`")
# Adding stoploss and stoploss percentage only if it is not None
lines.append("*Stoploss:* `{stop_loss_abs:.8g}` " +
lines.append(f"*Stoploss:* `{round_value(r['stop_loss_abs'], 8)}` " +
("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else ""))
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8g}` "
lines.append(f"*Stoploss distance:* `{round_value(r['stoploss_current_dist'], 8)}` "
"`({stoploss_current_dist_ratio:.2%})`")
if r.get('open_orders'):
lines.append(
@ -1172,7 +1174,7 @@ class Telegram(RPCHandler):
text='Cancel', callback_data='force_exit__cancel')])
await self._send_msg(msg="Which trade?", keyboard=buttons_aligned)
async def _force_exit_action(self, trade_id):
async def _force_exit_action(self, trade_id: str):
if trade_id != 'cancel':
try:
loop = asyncio.get_running_loop()

View File

@ -373,6 +373,19 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return True
def order_filled(self, pair: str, trade: Trade, order: Order,
current_time: datetime, **kwargs) -> None:
"""
Called right after an order fills.
Will be called for all order types (entry, exit, stoploss, position adjustment).
:param pair: Pair for trade
:param trade: trade object.
:param order: Order object.
: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.
"""
pass
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, after_fill: bool, **kwargs) -> Optional[float]:
"""

View File

@ -300,3 +300,17 @@ def leverage(self, pair: str, current_time: datetime, current_rate: float,
:return: A leverage amount, which is between 1.0 and max_leverage.
"""
return 1.0
def order_filled(self, pair: str, trade: 'Trade', order: 'Order',
current_time: datetime, **kwargs) -> None:
"""
Called right after an order fills.
Will be called for all order types (entry, exit, stoploss, position adjustment).
:param pair: Pair for trade
:param trade: trade object.
:param order: Order object.
: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.
"""
pass

View File

@ -10,6 +10,15 @@ def decimals_per_coin(coin: str):
return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK)
def strip_trailing_zeros(value: str) -> str:
"""
Strip trailing zeros from a string
:param value: Value to be stripped
:return: Stripped value
"""
return value.rstrip('0').rstrip('.')
def round_value(value: float, decimals: int, keep_trailing_zeros=False) -> str:
"""
Round value to given decimals
@ -20,7 +29,7 @@ def round_value(value: float, decimals: int, keep_trailing_zeros=False) -> str:
"""
val = f"{value:.{decimals}f}"
if not keep_trailing_zeros:
val = val.rstrip('0').rstrip('.')
val = strip_trailing_zeros(val)
return val
@ -34,7 +43,6 @@ def fmt_coin(
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
:return: Formatted / rounded value (with or without coin name)
"""
val = f"{value:.{decimals_per_coin(coin)}f}"
val = round_value(value, decimals_per_coin(coin), keep_trailing_zeros)
if show_coin_name:
val = f"{val} {coin}"

1
ft_client/LICENSE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE

4
ft_client/MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
include LICENSE
include README.md
prune tests

7
ft_client/README.md Normal file
View File

@ -0,0 +1,7 @@
# Freqtrade Client
# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg)
Provides a minimal rest client for the freqtrade rest api.
Please check out the [main project](https://github.com/freqtrade/freqtrade) for more information or details.

View File

@ -0,0 +1,26 @@
from freqtrade_client.ft_rest_client import FtRestClient
__version__ = '2024.4-dev'
if 'dev' in __version__:
from pathlib import Path
try:
import subprocess
freqtrade_basedir = Path(__file__).parent
__version__ = __version__ + '-' + subprocess.check_output(
['git', 'log', '--format="%h"', '-n 1'],
stderr=subprocess.DEVNULL, cwd=freqtrade_basedir).decode("utf-8").rstrip().strip('"')
except Exception: # pragma: no cover
# git not available, ignore
try:
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
versionfile = Path('./freqtrade_commit')
if versionfile.is_file():
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
except Exception:
pass
__all__ = ['FtRestClient']

View File

@ -0,0 +1,106 @@
import argparse
import inspect
import json
import logging
import re
import sys
from pathlib import Path
from typing import Any, Dict
import rapidjson
from freqtrade_client import __version__
from freqtrade_client.ft_rest_client import FtRestClient
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
logger = logging.getLogger("ft_rest_client")
def add_arguments(args: Any = None):
parser = argparse.ArgumentParser()
parser.add_argument("command",
help="Positional argument defining the command to execute.",
nargs="?"
)
parser.add_argument('-V', '--version', action='version', version=f'%(prog)s {__version__}')
parser.add_argument('--show',
help='Show possible methods with this client',
dest='show',
action='store_true',
default=False
)
parser.add_argument('-c', '--config',
help='Specify configuration file (default: %(default)s). ',
dest='config',
type=str,
metavar='PATH',
default='config.json'
)
parser.add_argument("command_arguments",
help="Positional arguments for the parameters for [command]",
nargs="*",
default=[]
)
pargs = parser.parse_args(args)
return vars(pargs)
def load_config(configfile):
file = Path(configfile)
if file.is_file():
with file.open("r") as f:
config = rapidjson.load(f, parse_mode=rapidjson.PM_COMMENTS |
rapidjson.PM_TRAILING_COMMAS)
return config
else:
logger.warning(f"Could not load config file {file}.")
sys.exit(1)
def print_commands():
# Print dynamic help for the different commands using the commands doc-strings
client = FtRestClient(None)
print("Possible commands:\n")
for x, y in inspect.getmembers(client):
if not x.startswith('_'):
doc = re.sub(':return:.*', '', getattr(client, x).__doc__, flags=re.MULTILINE).rstrip()
print(f"{x}\n\t{doc}\n")
def main_exec(args: Dict[str, Any]):
if args.get("show"):
print_commands()
sys.exit()
config = load_config(args['config'])
url = config.get('api_server', {}).get('listen_ip_address', '127.0.0.1')
port = config.get('api_server', {}).get('listen_port', '8080')
username = config.get('api_server', {}).get('username')
password = config.get('api_server', {}).get('password')
server_url = f"http://{url}:{port}"
client = FtRestClient(server_url, username, password)
m = [x for x, y in inspect.getmembers(client) if not x.startswith('_')]
command = args["command"]
if command not in m:
logger.error(f"Command {command} not defined")
print_commands()
return
print(json.dumps(getattr(client, command)(*args["command_arguments"])))
def main():
"""
Main entry point for the client
"""
args = add_arguments()
main_exec(args)

View File

@ -0,0 +1,446 @@
"""
A Rest Client for Freqtrade bot
Should not import anything from freqtrade,
so it can be used as a standalone script, and can be installed independently.
"""
import json
import logging
from typing import Any, Dict, List, Optional, Union
from urllib.parse import urlencode, urlparse, urlunparse
import requests
from requests.exceptions import ConnectionError
logger = logging.getLogger("ft_rest_client")
ParamsT = Optional[Dict[str, Any]]
PostDataT = Optional[Union[Dict[str, Any], List[Dict[str, Any]]]]
class FtRestClient:
def __init__(self, serverurl, username=None, password=None, *,
pool_connections=10, pool_maxsize=10):
self._serverurl = serverurl
self._session = requests.Session()
# allow configuration of pool
adapter = requests.adapters.HTTPAdapter(
pool_connections=pool_connections,
pool_maxsize=pool_maxsize
)
self._session.mount('http://', adapter)
self._session.auth = (username, password)
def _call(self, method, apipath, params: Optional[dict] = None, data=None, files=None):
if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'):
raise ValueError(f'invalid method <{method}>')
basepath = f"{self._serverurl}/api/v1/{apipath}"
hd = {"Accept": "application/json",
"Content-Type": "application/json"
}
# Split url
schema, netloc, path, par, query, fragment = urlparse(basepath)
# URLEncode query string
query = urlencode(params) if params else ""
# recombine url
url = urlunparse((schema, netloc, path, par, query, fragment))
try:
resp = self._session.request(method, url, headers=hd, data=json.dumps(data))
# return resp.text
return resp.json()
except ConnectionError:
logger.warning("Connection error")
def _get(self, apipath, params: ParamsT = None):
return self._call("GET", apipath, params=params)
def _delete(self, apipath, params: ParamsT = None):
return self._call("DELETE", apipath, params=params)
def _post(self, apipath, params: ParamsT = None, data: PostDataT = None):
return self._call("POST", apipath, params=params, data=data)
def start(self):
"""Start the bot if it's in the stopped state.
:return: json object
"""
return self._post("start")
def stop(self):
"""Stop the bot. Use `start` to restart.
:return: json object
"""
return self._post("stop")
def stopbuy(self):
"""Stop buying (but handle sells gracefully). Use `reload_config` to reset.
:return: json object
"""
return self._post("stopbuy")
def reload_config(self):
"""Reload configuration.
:return: json object
"""
return self._post("reload_config")
def balance(self):
"""Get the account balance.
:return: json object
"""
return self._get("balance")
def count(self):
"""Return the amount of open trades.
:return: json object
"""
return self._get("count")
def entries(self, pair=None):
"""Returns List of dicts containing all Trades, based on buy tag performance
Can either be average for all pairs or a specific pair provided
:return: json object
"""
return self._get("entries", params={"pair": pair} if pair else None)
def exits(self, pair=None):
"""Returns List of dicts containing all Trades, based on exit reason performance
Can either be average for all pairs or a specific pair provided
:return: json object
"""
return self._get("exits", params={"pair": pair} if pair else None)
def mix_tags(self, pair=None):
"""Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance
Can either be average for all pairs or a specific pair provided
:return: json object
"""
return self._get("mix_tags", params={"pair": pair} if pair else None)
def locks(self):
"""Return current locks
:return: json object
"""
return self._get("locks")
def delete_lock(self, lock_id):
"""Delete (disable) lock from the database.
:param lock_id: ID for the lock to delete
:return: json object
"""
return self._delete(f"locks/{lock_id}")
def lock_add(self, pair: str, until: str, side: str = '*', reason: str = ''):
"""Lock pair
:param pair: Pair to lock
:param until: Lock until this date (format "2024-03-30 16:00:00Z")
:param side: Side to lock (long, short, *)
:param reason: Reason for the lock
:return: json object
"""
data = [
{
"pair": pair,
"until": until,
"side": side,
"reason": reason
}
]
return self._post("locks", data=data)
def daily(self, days=None):
"""Return the profits for each day, and amount of trades.
:return: json object
"""
return self._get("daily", params={"timescale": days} if days else None)
def weekly(self, weeks=None):
"""Return the profits for each week, and amount of trades.
:return: json object
"""
return self._get("weekly", params={"timescale": weeks} if weeks else None)
def monthly(self, months=None):
"""Return the profits for each month, and amount of trades.
:return: json object
"""
return self._get("monthly", params={"timescale": months} if months else None)
def edge(self):
"""Return information about edge.
:return: json object
"""
return self._get("edge")
def profit(self):
"""Return the profit summary.
:return: json object
"""
return self._get("profit")
def stats(self):
"""Return the stats report (durations, sell-reasons).
:return: json object
"""
return self._get("stats")
def performance(self):
"""Return the performance of the different coins.
:return: json object
"""
return self._get("performance")
def status(self):
"""Get the status of open trades.
:return: json object
"""
return self._get("status")
def version(self):
"""Return the version of the bot.
:return: json object containing the version
"""
return self._get("version")
def show_config(self):
""" Returns part of the configuration, relevant for trading operations.
:return: json object containing the version
"""
return self._get("show_config")
def ping(self):
"""simple ping"""
configstatus = self.show_config()
if not configstatus:
return {"status": "not_running"}
elif configstatus['state'] == "running":
return {"status": "pong"}
else:
return {"status": "not_running"}
def logs(self, limit=None):
"""Show latest logs.
:param limit: Limits log messages to the last <limit> logs. No limit to get the entire log.
:return: json object
"""
return self._get("logs", params={"limit": limit} if limit else 0)
def trades(self, limit=None, offset=None):
"""Return trades history, sorted by id
:param limit: Limits trades to the X last trades. Max 500 trades.
:param offset: Offset by this amount of trades.
:return: json object
"""
params = {}
if limit:
params['limit'] = limit
if offset:
params['offset'] = offset
return self._get("trades", params)
def trade(self, trade_id):
"""Return specific trade
:param trade_id: Specify which trade to get.
:return: json object
"""
return self._get(f"trade/{trade_id}")
def delete_trade(self, trade_id):
"""Delete trade from the database.
Tries to close open orders. Requires manual handling of this asset on the exchange.
:param trade_id: Deletes the trade with this ID from the database.
:return: json object
"""
return self._delete(f"trades/{trade_id}")
def cancel_open_order(self, trade_id):
"""Cancel open order for trade.
:param trade_id: Cancels open orders for this trade.
:return: json object
"""
return self._delete(f"trades/{trade_id}/open-order")
def whitelist(self):
"""Show the current whitelist.
:return: json object
"""
return self._get("whitelist")
def blacklist(self, *args):
"""Show the current blacklist.
:param add: List of coins to add (example: "BNB/BTC")
:return: json object
"""
if not args:
return self._get("blacklist")
else:
return self._post("blacklist", data={"blacklist": args})
def forcebuy(self, pair, price=None):
"""Buy an asset.
:param pair: Pair to buy (ETH/BTC)
:param price: Optional - price to buy
:return: json object of the trade
"""
data = {"pair": pair,
"price": price
}
return self._post("forcebuy", data=data)
def forceenter(self, pair, side, price=None):
"""Force entering a trade
:param pair: Pair to buy (ETH/BTC)
:param side: 'long' or 'short'
:param price: Optional - price to buy
:return: json object of the trade
"""
data = {"pair": pair,
"side": side,
}
if price:
data['price'] = price
return self._post("forceenter", data=data)
def forceexit(self, tradeid, ordertype=None, amount=None):
"""Force-exit a trade.
:param tradeid: Id of the trade (can be received via status command)
:param ordertype: Order type to use (must be market or limit)
:param amount: Amount to sell. Full sell if not given
:return: json object
"""
return self._post("forceexit", data={
"tradeid": tradeid,
"ordertype": ordertype,
"amount": amount,
})
def strategies(self):
"""Lists available strategies
:return: json object
"""
return self._get("strategies")
def strategy(self, strategy):
"""Get strategy details
:param strategy: Strategy class name
:return: json object
"""
return self._get(f"strategy/{strategy}")
def pairlists_available(self):
"""Lists available pairlist providers
:return: json object
"""
return self._get("pairlists/available")
def plot_config(self):
"""Return plot configuration if the strategy defines one.
:return: json object
"""
return self._get("plot_config")
def available_pairs(self, timeframe=None, stake_currency=None):
"""Return available pair (backtest data) based on timeframe / stake_currency selection
:param timeframe: Only pairs with this timeframe available.
:param stake_currency: Only pairs that include this timeframe
:return: json object
"""
return self._get("available_pairs", params={
"stake_currency": stake_currency if timeframe else '',
"timeframe": timeframe if timeframe else '',
})
def pair_candles(self, pair, timeframe, limit=None):
"""Return live dataframe for <pair><timeframe>.
:param pair: Pair to get data for
:param timeframe: Only pairs with this timeframe available.
:param limit: Limit result to the last n candles.
:return: json object
"""
params = {
"pair": pair,
"timeframe": timeframe,
}
if limit:
params['limit'] = limit
return self._get("pair_candles", params=params)
def pair_history(self, pair, timeframe, strategy, timerange=None, freqaimodel=None):
"""Return historic, analyzed dataframe
:param pair: Pair to get data for
:param timeframe: Only pairs with this timeframe available.
:param strategy: Strategy to analyze and get values for
:param freqaimodel: FreqAI model to use for analysis
:param timerange: Timerange to get data for (same format than --timerange endpoints)
:return: json object
"""
return self._get("pair_history", params={
"pair": pair,
"timeframe": timeframe,
"strategy": strategy,
"freqaimodel": freqaimodel,
"timerange": timerange if timerange else '',
})
def sysinfo(self):
"""Provides system information (CPU, RAM usage)
:return: json object
"""
return self._get("sysinfo")
def health(self):
"""Provides a quick health check of the running bot.
:return: json object
"""
return self._get("health")

54
ft_client/pyproject.toml Normal file
View File

@ -0,0 +1,54 @@
[build-system]
requires = ["setuptools >= 64.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "freqtrade-client"
dynamic = ["version"]
authors = [
{name = "Freqtrade Team"},
{name = "Freqtrade Team", email = "freqtrade@protonmail.com"},
]
description = "Freqtrade - Client scripts"
readme = "README.md"
requires-python = ">=3.9"
license = {text = "GPLv3"}
# license = "GPLv3"
classifiers = [
"Environment :: Console",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Operating System :: MacOS",
"Operating System :: Unix",
"Topic :: Office/Business :: Financial :: Investment",
]
dependencies = [
'requests >= 2.26.0',
'python-rapidjson >= 1.0',
]
[project.urls]
Homepage = "https://github.com/freqtrade/freqtrade"
Documentation = "https://freqtrade.io"
"Bug Tracker" = "https://github.com/freqtrade/freqtrade/issues"
[project.scripts]
freqtrade-client = "freqtrade_client.ft_client:main"
[tool.setuptools.packages.find]
where = ["."]
include = ["freqtrade_client*"]
exclude = ["tests", "tests.*"]
namespaces = true
[tool.setuptools.dynamic]
version = {attr = "freqtrade_client.__version__"}

View File

@ -0,0 +1,3 @@
# Requirements for freqtrade client library
requests==2.31.0
python-rapidjson==1.16

View File

View File

@ -0,0 +1,153 @@
import re
from unittest.mock import MagicMock
import pytest
from freqtrade_client import FtRestClient
from freqtrade_client.ft_client import add_arguments, main_exec
from requests.exceptions import ConnectionError
def log_has_re(line, logs):
"""Check if line matches some caplog's message."""
return any(re.match(line, message) for message in logs.messages)
def get_rest_client():
client = FtRestClient('http://localhost:8080', 'freqtrader', 'password')
client._session = MagicMock()
request_mock = MagicMock()
client._session.request = request_mock
return client, request_mock
def test_FtRestClient_init():
client = FtRestClient('http://localhost:8080', 'freqtrader', 'password')
assert client is not None
assert client._serverurl == 'http://localhost:8080'
assert client._session is not None
assert client._session.auth is not None
assert client._session.auth == ('freqtrader', 'password')
@pytest.mark.parametrize('method', ['GET', 'POST', 'DELETE'])
def test_FtRestClient_call(method):
client, mock = get_rest_client()
client._call(method, '/dummytest')
assert mock.call_count == 1
getattr(client, f"_{method.lower()}")('/dummytest')
assert mock.call_count == 2
def test_FtRestClient_call_invalid(caplog):
client, _ = get_rest_client()
with pytest.raises(ValueError):
client._call('PUTTY', '/dummytest')
client._session.request = MagicMock(side_effect=ConnectionError())
client._call('GET', '/dummytest')
assert log_has_re('Connection error', caplog)
@pytest.mark.parametrize('method,args', [
('start', []),
('stop', []),
('stopbuy', []),
('reload_config', []),
('balance', []),
('count', []),
('entries', []),
('exits', []),
('mix_tags', []),
('locks', []),
('lock_add', ["XRP/USDT", '2024-01-01 20:00:00Z', '*', 'rand']),
('delete_lock', [2]),
('daily', []),
('daily', [15]),
('weekly', []),
('weekly', [15]),
('monthly', []),
('monthly', [12]),
('edge', []),
('profit', []),
('stats', []),
('performance', []),
('status', []),
('version', []),
('show_config', []),
('ping', []),
('logs', []),
('logs', [55]),
('trades', []),
('trades', [5]),
('trades', [5, 5]), # With offset
('trade', [1]),
('delete_trade', [1]),
('cancel_open_order', [1]),
('whitelist', []),
('blacklist', []),
('blacklist', ['XRP/USDT']),
('blacklist', ['XRP/USDT', 'BTC/USDT']),
('forcebuy', ['XRP/USDT']),
('forcebuy', ['XRP/USDT', 1.5]),
('forceenter', ['XRP/USDT', 'short']),
('forceenter', ['XRP/USDT', 'short', 1.5]),
('forceexit', [1]),
('forceexit', [1, 'limit']),
('forceexit', [1, 'limit', 100]),
('strategies', []),
('strategy', ['sampleStrategy']),
('pairlists_available', []),
('plot_config', []),
('available_pairs', []),
('available_pairs', ['5m']),
('pair_candles', ['XRP/USDT', '5m']),
('pair_candles', ['XRP/USDT', '5m', 500]),
('pair_history', ['XRP/USDT', '5m', 'SampleStrategy']),
('sysinfo', []),
('health', []),
])
def test_FtRestClient_call_explicit_methods(method, args):
client, mock = get_rest_client()
exec = getattr(client, method)
exec(*args)
assert mock.call_count == 1
def test_ft_client(mocker, capsys, caplog):
with pytest.raises(SystemExit):
args = add_arguments(['-V'])
args = add_arguments(['--show'])
assert isinstance(args, dict)
assert args['show'] is True
with pytest.raises(SystemExit):
main_exec(args)
captured = capsys.readouterr()
assert 'Possible commands' in captured.out
mock = mocker.patch('freqtrade_client.ft_client.FtRestClient._call')
args = add_arguments([
'--config',
'tests/testdata/testconfigs/main_test_config.json',
'ping'
])
main_exec(args)
captured = capsys.readouterr()
assert mock.call_count == 1
with pytest.raises(SystemExit):
args = add_arguments(['--config', 'tests/testdata/testconfigs/nonexisting.json'])
main_exec(args)
assert log_has_re(r'Could not load config file .*nonexisting\.json\.',
caplog)
args = add_arguments([
'--config',
'tests/testdata/testconfigs/main_test_config.json',
'whatever'
])
main_exec(args)
assert log_has_re('Command whatever not defined', caplog)

View File

@ -1,5 +1,5 @@
site_name: Freqtrade
site_url: https://www.freqtrade.io/
site_url: https://www.freqtrade.io/en/latest/
repo_url: https://github.com/freqtrade/freqtrade
edit_uri: edit/develop/docs/
use_directory_urls: True

View File

@ -23,6 +23,7 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Operating System :: MacOS",
"Operating System :: Unix",
"Topic :: Office/Business :: Financial :: Investment",
@ -45,7 +46,7 @@ zip-safe = false
[tool.setuptools.packages.find]
where = ["."]
include = ["freqtrade*"]
exclude = ["tests", "tests.*"]
exclude = ["tests", "tests.*", "user_data", "user_data*"]
namespaces = true
[tool.setuptools.dynamic]
@ -87,7 +88,8 @@ ignore_missing_imports = true
namespace_packages = false
warn_unused_ignores = true
exclude = [
'^build_helpers\.py$'
'^build_helpers\.py$',
'^ft_client/build/.*$',
]
plugins = [
"sqlalchemy.ext.mypy.plugin"

View File

@ -7,25 +7,25 @@
-r docs/requirements-docs.txt
coveralls==3.3.1
ruff==0.3.2
ruff==0.3.5
mypy==1.9.0
pre-commit==3.6.2
pre-commit==3.7.0
pytest==8.1.1
pytest-asyncio==0.23.5.post1
pytest-cov==4.1.0
pytest-mock==3.12.0
pytest-asyncio==0.23.6
pytest-cov==5.0.0
pytest-mock==3.14.0
pytest-random-order==1.1.1
pytest-xdist==3.5.0
isort==5.13.2
# For datetime mocking
time-machine==2.14.0
time-machine==2.14.1
# Convert jupyter notebooks to markdown documents
nbconvert==7.16.2
nbconvert==7.16.3
# mypy types
types-cachetools==5.3.0.7
types-filelock==3.2.7
types-requests==2.31.0.20240311
types-tabulate==0.9.0.20240106
types-python-dateutil==2.8.19.20240311
types-python-dateutil==2.9.0.20240316

View File

@ -2,7 +2,7 @@
-r requirements-freqai.txt
# Required for freqai-rl
torch==2.2.1
torch==2.2.2
gymnasium==0.29.1
stable_baselines3==2.2.1
sb3_contrib>=2.2.1

View File

@ -5,4 +5,4 @@
scipy==1.12.0
scikit-learn==1.4.1.post1
ft-scikit-optimize==0.9.2
filelock==3.13.1
filelock==3.13.3

View File

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

View File

@ -2,10 +2,10 @@ numpy==1.26.4
pandas==2.2.1
pandas-ta==0.3.14b
ccxt==4.2.67
ccxt==4.2.87
cryptography==42.0.5
aiohttp==3.9.3
SQLAlchemy==2.0.28
SQLAlchemy==2.0.29
python-telegram-bot==21.0.1
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
@ -22,7 +22,7 @@ jinja2==3.1.3
tables==3.9.1
joblib==1.3.2
rich==13.7.1
pyarrow==15.0.1; platform_machine != 'armv7l'
pyarrow==15.0.2; platform_machine != 'armv7l'
# find first, C search in arrays
py_find_1st==1.1.6
@ -30,15 +30,15 @@ py_find_1st==1.1.6
# Load ticker files 30% faster
python-rapidjson==1.16
# Properly format api responses
orjson==3.9.15
orjson==3.10.0
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.110.0
pydantic==2.6.3
uvicorn==0.28.0
pydantic==2.6.4
uvicorn==0.29.0
pyjwt==2.8.0
aiofiles==23.2.1
psutil==5.9.8
@ -59,5 +59,5 @@ schedule==1.2.1
websockets==12.0
janus==1.0.0
ast-comments==1.2.1
ast-comments==1.2.2
packaging==24.0

View File

@ -7,505 +7,8 @@ Should not import anything from freqtrade,
so it can be used as a standalone script.
"""
import argparse
import inspect
import json
import logging
import re
import sys
from pathlib import Path
from typing import Optional
from urllib.parse import urlencode, urlparse, urlunparse
from freqtrade_client.ft_client import main
import rapidjson
import requests
from requests.exceptions import ConnectionError
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
logger = logging.getLogger("ft_rest_client")
class FtRestClient:
def __init__(self, serverurl, username=None, password=None):
self._serverurl = serverurl
self._session = requests.Session()
self._session.auth = (username, password)
def _call(self, method, apipath, params: Optional[dict] = None, data=None, files=None):
if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'):
raise ValueError(f'invalid method <{method}>')
basepath = f"{self._serverurl}/api/v1/{apipath}"
hd = {"Accept": "application/json",
"Content-Type": "application/json"
}
# Split url
schema, netloc, path, par, query, fragment = urlparse(basepath)
# URLEncode query string
query = urlencode(params) if params else ""
# recombine url
url = urlunparse((schema, netloc, path, par, query, fragment))
try:
resp = self._session.request(method, url, headers=hd, data=json.dumps(data))
# return resp.text
return resp.json()
except ConnectionError:
logger.warning("Connection error")
def _get(self, apipath, params: Optional[dict] = None):
return self._call("GET", apipath, params=params)
def _delete(self, apipath, params: Optional[dict] = None):
return self._call("DELETE", apipath, params=params)
def _post(self, apipath, params: Optional[dict] = None, data: Optional[dict] = None):
return self._call("POST", apipath, params=params, data=data)
def start(self):
"""Start the bot if it's in the stopped state.
:return: json object
"""
return self._post("start")
def stop(self):
"""Stop the bot. Use `start` to restart.
:return: json object
"""
return self._post("stop")
def stopbuy(self):
"""Stop buying (but handle sells gracefully). Use `reload_config` to reset.
:return: json object
"""
return self._post("stopbuy")
def reload_config(self):
"""Reload configuration.
:return: json object
"""
return self._post("reload_config")
def balance(self):
"""Get the account balance.
:return: json object
"""
return self._get("balance")
def count(self):
"""Return the amount of open trades.
:return: json object
"""
return self._get("count")
def entries(self, pair=None):
"""Returns List of dicts containing all Trades, based on buy tag performance
Can either be average for all pairs or a specific pair provided
:return: json object
"""
return self._get("entries", params={"pair": pair} if pair else None)
def exits(self, pair=None):
"""Returns List of dicts containing all Trades, based on exit reason performance
Can either be average for all pairs or a specific pair provided
:return: json object
"""
return self._get("exits", params={"pair": pair} if pair else None)
def mix_tags(self, pair=None):
"""Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance
Can either be average for all pairs or a specific pair provided
:return: json object
"""
return self._get("mix_tags", params={"pair": pair} if pair else None)
def locks(self):
"""Return current locks
:return: json object
"""
return self._get("locks")
def delete_lock(self, lock_id):
"""Delete (disable) lock from the database.
:param lock_id: ID for the lock to delete
:return: json object
"""
return self._delete(f"locks/{lock_id}")
def daily(self, days=None):
"""Return the profits for each day, and amount of trades.
:return: json object
"""
return self._get("daily", params={"timescale": days} if days else None)
def weekly(self, weeks=None):
"""Return the profits for each week, and amount of trades.
:return: json object
"""
return self._get("weekly", params={"timescale": weeks} if weeks else None)
def monthly(self, months=None):
"""Return the profits for each month, and amount of trades.
:return: json object
"""
return self._get("monthly", params={"timescale": months} if months else None)
def edge(self):
"""Return information about edge.
:return: json object
"""
return self._get("edge")
def profit(self):
"""Return the profit summary.
:return: json object
"""
return self._get("profit")
def stats(self):
"""Return the stats report (durations, sell-reasons).
:return: json object
"""
return self._get("stats")
def performance(self):
"""Return the performance of the different coins.
:return: json object
"""
return self._get("performance")
def status(self):
"""Get the status of open trades.
:return: json object
"""
return self._get("status")
def version(self):
"""Return the version of the bot.
:return: json object containing the version
"""
return self._get("version")
def show_config(self):
""" Returns part of the configuration, relevant for trading operations.
:return: json object containing the version
"""
return self._get("show_config")
def ping(self):
"""simple ping"""
configstatus = self.show_config()
if not configstatus:
return {"status": "not_running"}
elif configstatus['state'] == "running":
return {"status": "pong"}
else:
return {"status": "not_running"}
def logs(self, limit=None):
"""Show latest logs.
:param limit: Limits log messages to the last <limit> logs. No limit to get the entire log.
:return: json object
"""
return self._get("logs", params={"limit": limit} if limit else 0)
def trades(self, limit=None, offset=None):
"""Return trades history, sorted by id
:param limit: Limits trades to the X last trades. Max 500 trades.
:param offset: Offset by this amount of trades.
:return: json object
"""
params = {}
if limit:
params['limit'] = limit
if offset:
params['offset'] = offset
return self._get("trades", params)
def trade(self, trade_id):
"""Return specific trade
:param trade_id: Specify which trade to get.
:return: json object
"""
return self._get(f"trade/{trade_id}")
def delete_trade(self, trade_id):
"""Delete trade from the database.
Tries to close open orders. Requires manual handling of this asset on the exchange.
:param trade_id: Deletes the trade with this ID from the database.
:return: json object
"""
return self._delete(f"trades/{trade_id}")
def cancel_open_order(self, trade_id):
"""Cancel open order for trade.
:param trade_id: Cancels open orders for this trade.
:return: json object
"""
return self._delete(f"trades/{trade_id}/open-order")
def whitelist(self):
"""Show the current whitelist.
:return: json object
"""
return self._get("whitelist")
def blacklist(self, *args):
"""Show the current blacklist.
:param add: List of coins to add (example: "BNB/BTC")
:return: json object
"""
if not args:
return self._get("blacklist")
else:
return self._post("blacklist", data={"blacklist": args})
def forcebuy(self, pair, price=None):
"""Buy an asset.
:param pair: Pair to buy (ETH/BTC)
:param price: Optional - price to buy
:return: json object of the trade
"""
data = {"pair": pair,
"price": price
}
return self._post("forcebuy", data=data)
def forceenter(self, pair, side, price=None):
"""Force entering a trade
:param pair: Pair to buy (ETH/BTC)
:param side: 'long' or 'short'
:param price: Optional - price to buy
:return: json object of the trade
"""
data = {"pair": pair,
"side": side,
}
if price:
data['price'] = price
return self._post("forceenter", data=data)
def forceexit(self, tradeid, ordertype=None, amount=None):
"""Force-exit a trade.
:param tradeid: Id of the trade (can be received via status command)
:param ordertype: Order type to use (must be market or limit)
:param amount: Amount to sell. Full sell if not given
:return: json object
"""
return self._post("forceexit", data={
"tradeid": tradeid,
"ordertype": ordertype,
"amount": amount,
})
def strategies(self):
"""Lists available strategies
:return: json object
"""
return self._get("strategies")
def strategy(self, strategy):
"""Get strategy details
:param strategy: Strategy class name
:return: json object
"""
return self._get(f"strategy/{strategy}")
def pairlists_available(self):
"""Lists available pairlist providers
:return: json object
"""
return self._get("pairlists/available")
def plot_config(self):
"""Return plot configuration if the strategy defines one.
:return: json object
"""
return self._get("plot_config")
def available_pairs(self, timeframe=None, stake_currency=None):
"""Return available pair (backtest data) based on timeframe / stake_currency selection
:param timeframe: Only pairs with this timeframe available.
:param stake_currency: Only pairs that include this timeframe
:return: json object
"""
return self._get("available_pairs", params={
"stake_currency": stake_currency if timeframe else '',
"timeframe": timeframe if timeframe else '',
})
def pair_candles(self, pair, timeframe, limit=None):
"""Return live dataframe for <pair><timeframe>.
:param pair: Pair to get data for
:param timeframe: Only pairs with this timeframe available.
:param limit: Limit result to the last n candles.
:return: json object
"""
params = {
"pair": pair,
"timeframe": timeframe,
}
if limit:
params['limit'] = limit
return self._get("pair_candles", params=params)
def pair_history(self, pair, timeframe, strategy, timerange=None, freqaimodel=None):
"""Return historic, analyzed dataframe
:param pair: Pair to get data for
:param timeframe: Only pairs with this timeframe available.
:param strategy: Strategy to analyze and get values for
:param freqaimodel: FreqAI model to use for analysis
:param timerange: Timerange to get data for (same format than --timerange endpoints)
:return: json object
"""
return self._get("pair_history", params={
"pair": pair,
"timeframe": timeframe,
"strategy": strategy,
"freqaimodel": freqaimodel,
"timerange": timerange if timerange else '',
})
def sysinfo(self):
"""Provides system information (CPU, RAM usage)
:return: json object
"""
return self._get("sysinfo")
def health(self):
"""Provides a quick health check of the running bot.
:return: json object
"""
return self._get("health")
def add_arguments():
parser = argparse.ArgumentParser()
parser.add_argument("command",
help="Positional argument defining the command to execute.",
nargs="?"
)
parser.add_argument('--show',
help='Show possible methods with this client',
dest='show',
action='store_true',
default=False
)
parser.add_argument('-c', '--config',
help='Specify configuration file (default: %(default)s). ',
dest='config',
type=str,
metavar='PATH',
default='config.json'
)
parser.add_argument("command_arguments",
help="Positional arguments for the parameters for [command]",
nargs="*",
default=[]
)
args = parser.parse_args()
return vars(args)
def load_config(configfile):
file = Path(configfile)
if file.is_file():
with file.open("r") as f:
config = rapidjson.load(f, parse_mode=rapidjson.PM_COMMENTS |
rapidjson.PM_TRAILING_COMMAS)
return config
else:
logger.warning(f"Could not load config file {file}.")
sys.exit(1)
def print_commands():
# Print dynamic help for the different commands using the commands doc-strings
client = FtRestClient(None)
print("Possible commands:\n")
for x, y in inspect.getmembers(client):
if not x.startswith('_'):
doc = re.sub(':return:.*', '', getattr(client, x).__doc__, flags=re.MULTILINE).rstrip()
print(f"{x}\n\t{doc}\n")
def main(args):
if args.get("show"):
print_commands()
sys.exit()
config = load_config(args['config'])
url = config.get('api_server', {}).get('listen_ip_address', '127.0.0.1')
port = config.get('api_server', {}).get('listen_port', '8080')
username = config.get('api_server', {}).get('username')
password = config.get('api_server', {}).get('password')
server_url = f"http://{url}:{port}"
client = FtRestClient(server_url, username, password)
m = [x for x, y in inspect.getmembers(client) if not x.startswith('_')]
command = args["command"]
if command not in m:
logger.error(f"Command {command} not defined")
print_commands()
return
print(json.dumps(getattr(client, command)(*args["command_arguments"])))
if __name__ == "__main__":
args = add_arguments()
main(args)
if __name__ == '__main__':
main()

View File

@ -80,7 +80,7 @@ setup(
'urllib3',
'jsonschema',
'numpy',
'pandas',
'pandas>=2.2.0,<3.0',
'TA-Lib',
'pandas-ta',
'technical',
@ -112,6 +112,7 @@ setup(
'python-dateutil',
'pytz',
'packaging',
'freqtrade-client',
],
extras_require={
'dev': all_extra,

View File

@ -12,9 +12,9 @@ from freqtrade.commands import (start_backtesting_show, start_convert_data, star
start_create_userdir, start_download_data, start_hyperopt_list,
start_hyperopt_show, start_install_ui, start_list_data,
start_list_exchanges, start_list_markets, start_list_strategies,
start_list_timeframes, start_new_strategy, start_show_trades,
start_strategy_update, start_test_pairlist, start_trading,
start_webserver)
start_list_timeframes, start_new_strategy, start_show_config,
start_show_trades, start_strategy_update, start_test_pairlist,
start_trading, start_webserver)
from freqtrade.commands.db_commands import start_convert_db
from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui,
get_ui_download_url, read_ui_version)
@ -39,6 +39,14 @@ def test_setup_utils_configuration():
assert "exchange" in config
assert config['dry_run'] is True
args = [
'list-exchanges', '--config', 'tests/testdata/testconfigs/testconfig.json',
]
config = setup_utils_configuration(get_args(args), RunMode.OTHER, set_dry=False)
assert "exchange" in config
assert config['dry_run'] is False
def test_start_trading_fail(mocker, caplog):
@ -51,15 +59,16 @@ def test_start_trading_fail(mocker, caplog):
'trade',
'-c', 'tests/testdata/testconfigs/main_test_config.json'
]
start_trading(get_args(args))
with pytest.raises(OperationalException):
start_trading(get_args(args))
assert exitmock.call_count == 1
exitmock.reset_mock()
caplog.clear()
mocker.patch("freqtrade.worker.Worker.__init__", MagicMock(side_effect=OperationalException))
start_trading(get_args(args))
with pytest.raises(OperationalException):
start_trading(get_args(args))
assert exitmock.call_count == 0
assert log_has('Fatal exception!', caplog)
def test_start_webserver(mocker, caplog):
@ -1571,3 +1580,33 @@ def test_start_strategy_updater(mocker, tmp_path):
start_strategy_update(pargs)
# Number of strategies in the test directory
assert sc_mock.call_count == 2
def test_start_show_config(capsys, caplog):
args = [
"show-config",
"--config",
"tests/testdata/testconfigs/main_test_config.json",
]
pargs = get_args(args)
start_show_config(pargs)
captured = capsys.readouterr()
assert "Your combined configuration is:" in captured.out
assert '"max_open_trades":' in captured.out
assert '"secret": "REDACTED"' in captured.out
args = [
"show-config",
"--config",
"tests/testdata/testconfigs/main_test_config.json",
"--show-sensitive"
]
pargs = get_args(args)
start_show_config(pargs)
captured = capsys.readouterr()
assert "Your combined configuration is:" in captured.out
assert '"max_open_trades":' in captured.out
assert '"secret": "REDACTED"' not in captured.out
assert log_has_re(r'Sensitive information will be shown in the upcomming output.*', caplog)

View File

@ -11,8 +11,8 @@ from numpy import NaN
from pandas import DataFrame
from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode
from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError,
InsufficientFundsError, InvalidOrderException,
from freqtrade.exceptions import (ConfigurationError, DDosProtection, DependencyException,
ExchangeError, InsufficientFundsError, InvalidOrderException,
OperationalException, PricingError, TemporaryError)
from freqtrade.exchange import (Binance, Bybit, Exchange, Kraken, market_is_active,
timeframe_to_prev_date)
@ -595,7 +595,7 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog):
mocker.patch(f'{EXMS}.validate_pairs')
mocker.patch(f'{EXMS}.validate_timeframes')
mocker.patch(f'{EXMS}._load_async_markets')
with pytest.raises(OperationalException,
with pytest.raises(ConfigurationError,
match=r'XRP is not available as stake on .*'
'Available currencies are: BTC, ETH, USDT'):
Exchange(default_conf)
@ -800,12 +800,12 @@ def test_validate_timeframes_failed(default_conf, mocker):
mocker.patch(f'{EXMS}.validate_pairs')
mocker.patch(f'{EXMS}.validate_stakecurrency')
mocker.patch(f'{EXMS}.validate_pricing')
with pytest.raises(OperationalException,
with pytest.raises(ConfigurationError,
match=r"Invalid timeframe '3m'. This exchange supports.*"):
Exchange(default_conf)
default_conf["timeframe"] = "15s"
with pytest.raises(OperationalException,
with pytest.raises(ConfigurationError,
match=r"Timeframes < 1m are currently not supported by Freqtrade."):
Exchange(default_conf)
@ -1066,6 +1066,9 @@ def test_exchange_has(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert not exchange.exchange_has("deadbeef")
exchange._ft_has['exchange_has_overrides'] = {'deadbeef': True}
assert exchange.exchange_has("deadbeef")
@pytest.mark.parametrize("side,leverage", [
("buy", 1),

View File

@ -262,6 +262,13 @@ EXCHANGES = {
'leverage_tiers_public': False,
'leverage_in_spot_market': False,
},
'bingx': {
'pair': 'BTC/USDT',
'stake_currency': 'USDT',
'hasQuoteVolume': True,
'timeframe': '1h',
'futures': False,
},
}

View File

@ -1233,6 +1233,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
order_id=order_id,
))
freqtrade.strategy.order_filled = MagicMock(return_value=None)
assert not freqtrade.update_trade_state(trade, None)
assert log_has_re(r'Orderid for trade .* is empty.', caplog)
caplog.clear()
@ -1243,6 +1244,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
caplog.clear()
assert not trade.has_open_orders
assert trade.amount == order['amount']
assert freqtrade.strategy.order_filled.call_count == 1
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=0.01)
assert trade.amount == 30.0
@ -1260,11 +1262,13 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
limit_buy_order_usdt_new['filled'] = 0.0
limit_buy_order_usdt_new['status'] = 'canceled'
freqtrade.strategy.order_filled = MagicMock(return_value=None)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=ValueError)
mocker.patch(f'{EXMS}.fetch_order', return_value=limit_buy_order_usdt_new)
res = freqtrade.update_trade_state(trade, order_id)
# Cancelled empty
assert res is True
assert freqtrade.strategy.order_filled.call_count == 0
@pytest.mark.parametrize("is_short", [False, True])
@ -5460,9 +5464,10 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca
assert freqtrade.strategy.adjust_trade_position.call_count == 1
caplog.clear()
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(-10, 'partial_exit_c'))
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(-0.0005, 'partial_exit_c'))
freqtrade.process_open_trade_positions()
assert log_has_re(r"LIMIT_SELL has been fulfilled.*", caplog)
assert freqtrade.strategy.adjust_trade_position.call_count == 1
trade = Trade.get_trades(trade_filter=[Trade.id == 5]).first()
assert trade.orders[-1].ft_order_tag == 'partial_exit_c'
assert trade.is_open

View File

@ -636,12 +636,12 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
assert len(trade.orders) == 2
assert trade.orders[-1].ft_order_side == 'sell'
assert trade.orders[-1].ft_order_tag == 'PES'
assert pytest.approx(trade.stake_amount) == 40.198
assert pytest.approx(trade.amount) == 20.099 * leverage
assert pytest.approx(trade.stake_amount) == 40
assert pytest.approx(trade.amount) == 20 * leverage
assert trade.open_rate == 2.0
assert trade.is_open
assert trade.realized_profit > 0.098 * leverage
expected_profit = starting_amount - 40.1980 + trade.realized_profit
expected_profit = starting_amount - 40 + trade.realized_profit
assert pytest.approx(freqtrade.wallets.get_free('USDT')) == expected_profit
if spot:
@ -667,14 +667,14 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
# Amount exactly comes out as exactly 0
freqtrade.strategy.adjust_trade_position = MagicMock(
return_value=-(trade.amount / trade.leverage * 2.02))
return_value=-trade.stake_amount)
freqtrade.process()
trade = Trade.get_trades().first()
assert len(trade.orders) == 3
assert trade.orders[-1].ft_order_side == 'sell'
assert pytest.approx(trade.stake_amount) == 40.198
assert pytest.approx(trade.stake_amount) == 40
assert trade.is_open is False
# use amount that would trunc to 0.0 once selling
@ -684,7 +684,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
trade = Trade.get_trades().first()
assert len(trade.orders) == 3
assert trade.orders[-1].ft_order_side == 'sell'
assert pytest.approx(trade.stake_amount) == 40.198
assert pytest.approx(trade.stake_amount) == 40
assert trade.is_open is False
assert log_has_re('Amount to exit is 0.0 due to exchange limits - not exiting.', caplog)
expected_profit = starting_amount - 60 + trade.realized_profit

View File

@ -146,10 +146,12 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
'amount': enter_order['amount'],
})
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
freqtrade.strategy.order_filled = MagicMock(return_value=None)
assert freqtrade.handle_stoploss_on_exchange(trade) is True
assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
assert len(trade.open_sl_orders) == 0
assert trade.is_open is False
assert freqtrade.strategy.order_filled.call_count == 1
caplog.clear()
mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError())

View File

@ -698,6 +698,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=timerange)
processed = backtesting.strategy.advise_all_indicators(data)
backtesting.strategy.order_filled = MagicMock()
min_date, max_date = get_timerange(processed)
result = backtesting.backtest(
@ -760,6 +761,8 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
pd.testing.assert_frame_equal(results, expected)
assert 'orders' in results.columns
data_pair = processed[pair]
# Called once per order
assert backtesting.strategy.order_filled.call_count == 4
for _, t in results.iterrows():
assert len(t['orders']) == 2
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
@ -1470,7 +1473,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
PropertyMock(return_value=['UNITTEST/BTC']))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
text_table_mock = MagicMock()
sell_reason_mock = MagicMock()
tag_metrics_mock = MagicMock()
strattable_mock = MagicMock()
strat_summary = MagicMock()
@ -1480,7 +1483,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
)
mocker.patch.multiple('freqtrade.optimize.optimize_reports.optimize_reports',
generate_pair_metrics=MagicMock(),
generate_exit_reason_stats=sell_reason_mock,
generate_tag_metrics=tag_metrics_mock,
generate_strategy_comparison=strat_summary,
generate_daily_stats=MagicMock(),
)
@ -1505,7 +1508,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
assert backtestmock.call_count == 2
assert text_table_mock.call_count == 4
assert strattable_mock.call_count == 1
assert sell_reason_mock.call_count == 2
assert tag_metrics_mock.call_count == 4
assert strat_summary.call_count == 1
# check the logs, that will contain the backtest result

View File

@ -15,16 +15,16 @@ from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backte
from freqtrade.edge import PairInfo
from freqtrade.enums import ExitType
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats,
generate_edge_table, generate_exit_reason_stats,
generate_pair_metrics,
generate_edge_table, generate_pair_metrics,
generate_periodic_breakdown_stats,
generate_strategy_comparison,
generate_trading_stats, show_sorted_pairlist,
store_backtest_analysis_results,
store_backtest_stats, text_table_bt_results,
text_table_exit_reason, text_table_strategy)
text_table_strategy)
from freqtrade.optimize.optimize_reports.bt_output import text_table_tags
from freqtrade.optimize.optimize_reports.optimize_reports import (_get_resample_from_period,
calc_streak)
calc_streak, generate_tag_metrics)
from freqtrade.resolvers.strategy_resolver import StrategyResolver
from freqtrade.util import dt_ts
from freqtrade.util.datetime_helpers import dt_from_ts, dt_utc
@ -59,13 +59,13 @@ def test_text_table_bt_results():
)
result_str = (
'| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | '
'| Pair | Entries | Avg Profit % | Tot Profit BTC | '
'Tot Profit % | Avg Duration | Win Draw Loss Win% |\n'
'|---------+-----------+----------------+----------------+------------------+'
'|---------+-----------+----------------+------------------+'
'----------------+----------------+-------------------------|\n'
'| ETH/BTC | 3 | 8.33 | 25.00 | 0.50000000 | '
'| ETH/BTC | 3 | 8.33 | 0.50000000 | '
'12.50 | 0:20:00 | 2 0 1 66.7 |\n'
'| TOTAL | 3 | 8.33 | 25.00 | 0.50000000 | '
'| TOTAL | 3 | 8.33 | 0.50000000 | '
'12.50 | 0:20:00 | 2 0 1 66.7 |'
)
@ -392,20 +392,21 @@ def test_text_table_exit_reason():
)
result_str = (
'| Exit Reason | Exits | Win Draws Loss Win% | Avg Profit % | Cum Profit % |'
' Tot Profit BTC | Tot Profit % |\n'
'|---------------+---------+--------------------------+----------------+----------------+'
'------------------+----------------|\n'
'| roi | 2 | 2 0 0 100 | 15 | 30 |'
' 0.6 | 15 |\n'
'| stop_loss | 1 | 0 0 1 0 | -10 | -10 |'
' -0.2 | -5 |'
'| Exit Reason | Exits | Avg Profit % | Tot Profit BTC | Tot Profit % |'
' Avg Duration | Win Draw Loss Win% |\n'
'|---------------+---------+----------------+------------------+----------------+'
'----------------+-------------------------|\n'
'| roi | 2 | 15.00 | 0.60000000 | 2.73 |'
' 0:20:00 | 2 0 0 100 |\n'
'| stop_loss | 1 | -10.00 | -0.20000000 | -0.91 |'
' 0:10:00 | 0 0 1 0 |\n'
'| TOTAL | 3 | 6.67 | 0.40000000 | 1.82 |'
' 0:17:00 | 2 0 1 66.7 |'
)
exit_reason_stats = generate_exit_reason_stats(max_open_trades=2,
results=results)
assert text_table_exit_reason(exit_reason_stats=exit_reason_stats,
stake_currency='BTC') == result_str
exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=22,
results=results, skip_nan=False)
assert text_table_tags('exit_tag', exit_reason_stats, 'BTC') == result_str
def test_generate_sell_reason_stats():
@ -423,10 +424,10 @@ def test_generate_sell_reason_stats():
}
)
exit_reason_stats = generate_exit_reason_stats(max_open_trades=2,
results=results)
exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=22,
results=results, skip_nan=False)
roi_result = exit_reason_stats[0]
assert roi_result['exit_reason'] == 'roi'
assert roi_result['key'] == 'roi'
assert roi_result['trades'] == 2
assert pytest.approx(roi_result['profit_mean']) == 0.15
assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2)
@ -435,7 +436,7 @@ def test_generate_sell_reason_stats():
stop_result = exit_reason_stats[1]
assert stop_result['exit_reason'] == 'stop_loss'
assert stop_result['key'] == 'stop_loss'
assert stop_result['trades'] == 1
assert pytest.approx(stop_result['profit_mean']) == -0.1
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2)
@ -450,13 +451,13 @@ def test_text_table_strategy(testdatadir):
bt_res_data_comparison = bt_res_data.pop('strategy_comparison')
result_str = (
'| Strategy | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC |'
'| Strategy | Entries | Avg Profit % | Tot Profit BTC |'
' Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n'
'|----------------+-----------+----------------+----------------+------------------+'
'|----------------+-----------+----------------+------------------+'
'----------------+----------------+-------------------------+-----------------------|\n'
'| StrategyTestV2 | 179 | 0.08 | 14.39 | 0.02608550 |'
'| StrategyTestV2 | 179 | 0.08 | 0.02608550 |'
' 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |\n'
'| TestStrategy | 179 | 0.08 | 14.39 | 0.02608550 |'
'| TestStrategy | 179 | 0.08 | 0.02608550 |'
' 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |'
)

View File

@ -406,6 +406,14 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
([{"method": "VolumePairList", "number_assets": 5,
"sort_key": "quoteVolume", "min_value": 1250}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
# HOT, XRP and FUEL whitelisted because they are below 1300 quoteVolume.
([{"method": "VolumePairList", "number_assets": 5,
"sort_key": "quoteVolume", "max_value": 1300}],
"BTC", ['XRP/BTC', 'HOT/BTC', 'FUEL/BTC']),
# HOT, XRP whitelisted because they are between 100 and 1300 quoteVolume.
([{"method": "VolumePairList", "number_assets": 5,
"sort_key": "quoteVolume", "min_value": 100, "max_value": 1300}],
"BTC", ['XRP/BTC', 'HOT/BTC']),
# StaticPairlist only
([{"method": "StaticPairList"}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']),

View File

@ -11,7 +11,6 @@ from freqtrade.enums import SignalDirection, State, TradingMode
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Order, Trade
from freqtrade.persistence.key_value_store import set_startup_time
from freqtrade.persistence.pairlock_middleware import PairLocks
from freqtrade.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from tests.conftest import (EXMS, create_mock_trades, create_mock_trades_usdt,
@ -491,12 +490,12 @@ def test_rpc_balance_handle_error(default_conf, mocker):
rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency'])
def test_rpc_balance_handle(default_conf, mocker, tickers):
def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
mock_balance = {
'BTC': {
'free': 10.0,
'total': 12.0,
'used': 2.0,
'free': 0.01,
'total': 0.012,
'used': 0.002,
},
'ETH': {
'free': 1.0,
@ -504,8 +503,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
'used': 4.0,
},
'USDT': {
'free': 5.0,
'total': 10.0,
'free': 50.0,
'total': 100.0,
'used': 5.0,
}
}
@ -519,10 +518,10 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
"maintenanceMargin": 0.0,
"maintenanceMarginPercentage": 0.005,
"entryPrice": 0.0,
"notional": 100.0,
"notional": 10.0,
"leverage": 5.0,
"unrealizedPnl": 0.0,
"contracts": 100.0,
"contracts": 1.0,
"contractSize": 1,
"marginRatio": None,
"liquidationPrice": 0.0,
@ -536,9 +535,9 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
mocker.patch.multiple(
'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}),
get_price=MagicMock(return_value={'bitcoin': {'usd': 1.2}}),
)
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.2)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
EXMS,
@ -549,86 +548,86 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
get_valid_pair_combination=MagicMock(
side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}")
)
default_conf['dry_run'] = False
default_conf['trading_mode'] = 'futures'
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
default_conf_usdt['dry_run'] = False
default_conf_usdt['trading_mode'] = 'futures'
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency'])
assert pytest.approx(result['total']) == 30.30909624
assert pytest.approx(result['value']) == 454636.44360691
result = rpc._rpc_balance(
default_conf_usdt['stake_currency'], default_conf_usdt['fiat_display_currency'])
assert pytest.approx(result['total']) == 2824.83464
assert pytest.approx(result['value']) == 2824.83464 * 1.2
assert tickers.call_count == 1
assert tickers.call_args_list[0][1]['cached'] is True
assert 'USD' == result['symbol']
assert result['currencies'] == [
{
'currency': 'BTC',
'free': 10.0,
'balance': 12.0,
'used': 2.0,
'bot_owned': 9.9, # available stake - reducing by reserved amount
'est_stake': 10.0, # In futures mode, "free" is used here.
'est_stake_bot': 9.9,
'stake': 'BTC',
'is_position': False,
'leverage': 1.0,
'position': 0.0,
'free': 0.01,
'balance': 0.012,
'used': 0.002,
'bot_owned': 0,
'est_stake': 103.78464,
'est_stake_bot': 0,
'stake': 'USDT',
'side': 'long',
'is_bot_managed': True,
'leverage': 1,
'position': 0,
'is_bot_managed': False,
'is_position': False
},
{
'currency': 'ETH',
'free': 1.0,
'balance': 5.0,
'currency': 'ETH',
'bot_owned': 0,
'est_stake': 0.30794,
'est_stake_bot': 0,
'used': 4.0,
'stake': 'BTC',
'is_position': False,
'leverage': 1.0,
'position': 0.0,
'side': 'long',
'is_bot_managed': False,
},
{
'free': 5.0,
'balance': 10.0,
'currency': 'USDT',
'bot_owned': 0,
'est_stake': 0.0011562404610161968,
'est_stake': 2651.05,
'est_stake_bot': 0,
'used': 5.0,
'stake': 'BTC',
'is_position': False,
'leverage': 1.0,
'position': 0.0,
'stake': 'USDT',
'side': 'long',
'leverage': 1,
'position': 0,
'is_bot_managed': False,
'is_position': False
},
{
'currency': 'USDT',
'free': 50.0,
'balance': 100.0,
'used': 5.0,
'bot_owned': 49.5,
'est_stake': 50.0,
'est_stake_bot': 49.5,
'stake': 'USDT',
'side': 'long',
'leverage': 1,
'position': 0,
'is_bot_managed': True,
'is_position': False
},
{
'free': 0.0,
'balance': 0.0,
'currency': 'ETH/USDT:USDT',
'free': 0,
'balance': 0,
'used': 0,
'position': 10.0,
'est_stake': 20,
'est_stake_bot': 20,
'used': 0,
'stake': 'BTC',
'is_position': True,
'stake': 'USDT',
'leverage': 5.0,
'position': 1000.0,
'side': 'short',
'is_bot_managed': True,
'is_position': True
}
]
assert pytest.approx(result['total_bot']) == 29.9
assert pytest.approx(result['total']) == 30.309096
assert result['starting_capital'] == 10
# Very high starting capital ratio, because the futures position really has the wrong unit.
# TODO: improve this test (see comment above)
assert result['starting_capital_ratio'] == pytest.approx(1.98999999)
assert pytest.approx(result['total_bot']) == 69.5
assert pytest.approx(result['total']) == 2824.83464 # ETH stake is missing.
assert result['starting_capital'] == 50
assert result['starting_capital_ratio'] == pytest.approx(0.3899999)
def test_rpc_start(mocker, default_conf) -> None:
@ -1171,14 +1170,15 @@ def test_rpc_force_entry_wrong_mode(mocker, default_conf) -> None:
@pytest.mark.usefixtures("init_persistence")
def test_rpc_delete_lock(mocker, default_conf):
def test_rpc_add_and_delete_lock(mocker, default_conf):
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc = RPC(freqtradebot)
pair = 'ETH/BTC'
PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=4))
PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=5))
PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=10))
rpc._rpc_add_lock(pair, datetime.now(timezone.utc) + timedelta(minutes=4), '', '*')
rpc._rpc_add_lock(pair, datetime.now(timezone.utc) + timedelta(minutes=5), '', '*')
rpc._rpc_add_lock(pair, datetime.now(timezone.utc) + timedelta(minutes=10), '', '*')
locks = rpc._rpc_locks()
assert locks['lock_count'] == 3
locks1 = rpc._rpc_delete_lock(lockid=locks['locks'][0]['id'])

View File

@ -23,12 +23,13 @@ from freqtrade.enums import CandleType, RunMode, State, TradingMode
from freqtrade.exceptions import DependencyException, ExchangeError, OperationalException
from freqtrade.loggers import setup_logging, setup_logging_pre
from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence import Trade
from freqtrade.rpc import RPC
from freqtrade.rpc.api_server import ApiServer
from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
from freqtrade.rpc.api_server.webserver_bgwork import ApiBG
from freqtrade.util.datetime_helpers import format_date
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_mock_coro,
get_patched_freqtradebot, log_has, log_has_re, patch_get_signal)
@ -553,8 +554,19 @@ def test_api_locks(botclient):
assert rc.json()['lock_count'] == 0
assert rc.json()['lock_count'] == len(rc.json()['locks'])
PairLocks.lock_pair('ETH/BTC', datetime.now(timezone.utc) + timedelta(minutes=4), 'randreason')
PairLocks.lock_pair('XRP/BTC', datetime.now(timezone.utc) + timedelta(minutes=20), 'deadbeef')
rc = client_post(client, f"{BASE_URI}/locks", [
{
"pair": "ETH/BTC",
"until": f"{format_date(datetime.now(timezone.utc) + timedelta(minutes=4))}Z",
"reason": "randreason"
}, {
"pair": "XRP/BTC",
"until": f"{format_date(datetime.now(timezone.utc) + timedelta(minutes=20))}Z",
"reason": "deadbeef"
}
])
assert_response(rc)
assert rc.json()['lock_count'] == 2
rc = client_get(client, f"{BASE_URI}/locks")
assert_response(rc)

View File

@ -795,9 +795,6 @@ def test_strategy_safe_wrapper_error(caplog, error):
def failing_method():
raise error('This is an error.')
def working_method(argumentpassedin):
return argumentpassedin
with pytest.raises(StrategyError, match=r'This is an error.'):
strategy_safe_wrapper(failing_method, message='DeadBeef')()

View File

@ -10,6 +10,7 @@ from jsonschema import ValidationError
from freqtrade.commands import Arguments
from freqtrade.configuration import Configuration, validate_config_consistency
from freqtrade.configuration.config_secrets import sanitize_config
from freqtrade.configuration.config_validation import validate_config_schema
from freqtrade.configuration.deprecated_settings import (check_conflicting_settings,
process_deprecated_setting,
@ -1440,7 +1441,7 @@ def test_flat_vars_to_nested_dict(caplog):
assert not log_has("Loading variable 'NOT_RELEVANT'", caplog)
def test_setup_hyperopt_freqai(mocker, default_conf, caplog) -> None:
def test_setup_hyperopt_freqai(mocker, default_conf) -> None:
patched_configuration_load_config_file(mocker, default_conf)
mocker.patch(
'freqtrade.configuration.configuration.create_datadir',
@ -1473,7 +1474,7 @@ def test_setup_hyperopt_freqai(mocker, default_conf, caplog) -> None:
validate_config_consistency(config)
def test_setup_freqai_backtesting(mocker, default_conf, caplog) -> None:
def test_setup_freqai_backtesting(mocker, default_conf) -> None:
patched_configuration_load_config_file(mocker, default_conf)
mocker.patch(
'freqtrade.configuration.configuration.create_datadir',
@ -1520,3 +1521,17 @@ def test_setup_freqai_backtesting(mocker, default_conf, caplog) -> None:
OperationalException, match=r".* pass --timerange if you intend to use FreqAI .*"
):
validate_config_consistency(conf)
def test_sanitize_config(default_conf_usdt):
assert default_conf_usdt['exchange']['key'] != 'REDACTED'
res = sanitize_config(default_conf_usdt)
# Didn't modify original dict
assert default_conf_usdt['exchange']['key'] != 'REDACTED'
assert res['exchange']['key'] == 'REDACTED'
assert res['exchange']['secret'] == 'REDACTED'
res = sanitize_config(default_conf_usdt, show_sensitive=True)
assert res['exchange']['key'] == default_conf_usdt['exchange']['key']
assert res['exchange']['secret'] == default_conf_usdt['exchange']['secret']

View File

@ -8,7 +8,7 @@ import pytest
from freqtrade.commands import Arguments
from freqtrade.enums import State
from freqtrade.exceptions import FreqtradeException, OperationalException
from freqtrade.exceptions import ConfigurationError, FreqtradeException, OperationalException
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.main import main
from freqtrade.worker import Worker
@ -141,6 +141,22 @@ def test_main_operational_exception1(mocker, default_conf, caplog) -> None:
assert log_has_re(r'SIGINT.*', caplog)
def test_main_ConfigurationError(mocker, default_conf, caplog) -> None:
patch_exchange(mocker)
mocker.patch(
'freqtrade.commands.list_commands.list_available_exchanges',
MagicMock(side_effect=ConfigurationError('Oh snap!'))
)
patched_configuration_load_config_file(mocker, default_conf)
args = ['list-exchanges']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit):
main(args)
assert log_has_re('Configuration error: Oh snap!', caplog)
def test_main_reload_config(mocker, default_conf, caplog) -> None:
patch_exchange(mocker)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())

View File

@ -1,6 +1,6 @@
{
"stake_currency": "",
"dry_run": true,
"dry_run": false,
"exchange": {
"name": "",
"key": "",