mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge branch 'develop' into feature/fetch-public-trades
This commit is contained in:
commit
63ac183e91
47
.github/workflows/binance-lev-tier-update.yml
vendored
Normal file
47
.github/workflows/binance-lev-tier-update.yml
vendored
Normal 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
|
42
.github/workflows/ci.yml
vendored
42
.github/workflows/ci.yml
vendored
|
@ -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'
|
||||
|
|
|
@ -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
|
1
.github/workflows/pre-commit-update.yml
vendored
1
.github/workflows/pre-commit-update.yml
vendored
|
@ -1,7 +1,6 @@
|
|||
name: Pre-commit auto-update
|
||||
|
||||
on:
|
||||
# every day at midnight
|
||||
schedule:
|
||||
- cron: "0 3 * * 2"
|
||||
# on demand
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
26
build_helpers/binance_update_lev_tiers.py
Normal file
26
build_helpers/binance_update_lev_tiers.py
Normal 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)
|
18
build_helpers/freqtrade_client_version_align.py
Executable file
18
build_helpers/freqtrade_client_version_align.py
Executable 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.
Binary file not shown.
BIN
docs/assets/show-config-output.png
Normal file
BIN
docs/assets/show-config-output.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -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
|
||||
|
|
|
@ -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,6 +49,8 @@ By default, the bot loop runs every few seconds (`internals.process_throttle_sec
|
|||
* Call `populate_indicators()`
|
||||
* Call `populate_entry_trend()`
|
||||
* Call `populate_exit_trend()`
|
||||
* 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.
|
||||
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ...]]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
""" Freqtrade bot """
|
||||
__version__ = '2024.3-dev'
|
||||
__version__ = '2024.4-dev'
|
||||
|
||||
if 'dev' in __version__:
|
||||
from pathlib import Path
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
|
|
|
@ -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. "
|
||||
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.")
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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`."
|
||||
)
|
||||
|
|
|
@ -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.")
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
36
freqtrade/configuration/config_secrets.py
Normal file
36
freqtrade/configuration/config_secrets.py
Normal 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
|
|
@ -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,6 +22,7 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str
|
|||
config = configuration.get_config()
|
||||
|
||||
# Ensure these modes are using Dry-run
|
||||
if set_dry:
|
||||
config['dry_run'] = True
|
||||
validate_config_consistency(config, preliminary=True)
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}"')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
19
freqtrade/exchange/bingx.py
Normal file
19
freqtrade/exchange/bingx.py
Normal 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,
|
||||
}
|
|
@ -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}")
|
||||
|
|
|
@ -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']
|
||||
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
order = self.fetch_order(
|
||||
|
@ -106,15 +104,17 @@ class Gate(Exchange):
|
|||
pair=pair,
|
||||
params={'stop': True}
|
||||
)
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
if order['status'] == 'closed':
|
||||
if order.get('status', 'open') == 'closed':
|
||||
# Places a real order - which we need to fetch explicitly.
|
||||
new_orderid = order.get('info', {}).get('trade_id')
|
||||
if new_orderid:
|
||||
val = 'trade_id' if self.trading_mode == TradingMode.FUTURES else 'fired_order_id'
|
||||
|
||||
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
|
||||
|
|
|
@ -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}. "
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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]:
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
1
ft_client/LICENSE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../LICENSE
|
4
ft_client/MANIFEST.in
Normal file
4
ft_client/MANIFEST.in
Normal file
|
@ -0,0 +1,4 @@
|
|||
include LICENSE
|
||||
include README.md
|
||||
|
||||
prune tests
|
7
ft_client/README.md
Normal file
7
ft_client/README.md
Normal 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.
|
26
ft_client/freqtrade_client/__init__.py
Normal file
26
ft_client/freqtrade_client/__init__.py
Normal 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']
|
106
ft_client/freqtrade_client/ft_client.py
Normal file
106
ft_client/freqtrade_client/ft_client.py
Normal 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)
|
446
ft_client/freqtrade_client/ft_rest_client.py
Executable file
446
ft_client/freqtrade_client/ft_rest_client.py
Executable 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
54
ft_client/pyproject.toml
Normal 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__"}
|
3
ft_client/requirements.txt
Normal file
3
ft_client/requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Requirements for freqtrade client library
|
||||
requests==2.31.0
|
||||
python-rapidjson==1.16
|
0
ft_client/test_client/__init__.py
Normal file
0
ft_client/test_client/__init__.py
Normal file
153
ft_client/test_client/test_rest_client.py
Normal file
153
ft_client/test_client/test_rest_client.py
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Include all requirements to run the bot.
|
||||
-r requirements.txt
|
||||
|
||||
plotly==5.19.0
|
||||
plotly==5.20.0
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
3
setup.py
3
setup.py
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
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))
|
||||
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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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% |'
|
||||
)
|
||||
|
||||
|
|
|
@ -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']),
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')()
|
||||
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"stake_currency": "",
|
||||
"dry_run": true,
|
||||
"dry_run": false,
|
||||
"exchange": {
|
||||
"name": "",
|
||||
"key": "",
|
||||
|
|
Loading…
Reference in New Issue
Block a user