Merge branch 'fix-bitget-stoploss' of https://github.com/ABSllk/freqtrade into fix-bitget-stoploss
This commit is contained in:
@@ -2,7 +2,7 @@ name: Binance Leverage tiers update
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "25 3 * * 4"
|
||||
- cron: "25 2 * * 4"
|
||||
# on demand
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -24,11 +24,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Install uv
|
||||
- name: Install uv and Python 🐍
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
activate-environment: true
|
||||
|
||||
@@ -32,12 +32,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python 🐍
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
- name: Install uv and Python 🐍
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
activate-environment: true
|
||||
@@ -177,12 +172,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python 🐍
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install uv
|
||||
- name: Install uv and Python 🐍
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
activate-environment: true
|
||||
@@ -201,7 +191,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- name: Set up Python 🐍
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
@@ -219,12 +210,7 @@ jobs:
|
||||
run: |
|
||||
./tests/test_docs.sh
|
||||
|
||||
- name: Set up Python 🐍
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install uv
|
||||
- name: Install uv and Python 🐍
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
activate-environment: true
|
||||
@@ -256,12 +242,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python 🐍
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "${{ matrix.python-version }}"
|
||||
|
||||
- name: Install uv
|
||||
- name: Install uv and Python 🐍
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
activate-environment: true
|
||||
@@ -328,12 +309,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python 🐍
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "${{ matrix.python-version }}"
|
||||
|
||||
- name: Install uv
|
||||
- name: Install uv and Python 🐍
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
activate-environment: true
|
||||
|
||||
@@ -26,12 +26,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install uv
|
||||
- name: Install uv and Python 🐍
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
activate-environment: true
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
uses: ./.github/actions/docker-tags
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -183,13 +183,13 @@ jobs:
|
||||
uses: ./.github/actions/docker-tags
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to github
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -25,11 +25,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install uv
|
||||
- name: Install uv and Python 🐍
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
activate-environment: true
|
||||
|
||||
@@ -15,7 +15,7 @@ repos:
|
||||
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.20.0"
|
||||
rev: "v1.20.1"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
@@ -26,12 +26,12 @@ repos:
|
||||
- types-tabulate==0.10.0.20260308
|
||||
- types-python-dateutil==2.9.0.20260402
|
||||
- scipy-stubs==1.17.1.3
|
||||
- SQLAlchemy==2.0.48
|
||||
- SQLAlchemy==2.0.49
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.15.9'
|
||||
rev: 'v0.15.10'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
@@ -70,6 +70,6 @@ repos:
|
||||
|
||||
# Ensure github actions remain safe
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.23.1
|
||||
rev: v1.24.1
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
+203
-170
@@ -160,118 +160,131 @@ The most important in the backtesting is to understand the result.
|
||||
A backtesting result will look like that:
|
||||
|
||||
```
|
||||
BACKTESTING REPORT
|
||||
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ LTC/USDT:USDT │ 16 │ 1.0 │ 56.176 │ 5.62 │ 16:16:00 │ 16 0 0 100 │
|
||||
│ ETC/USDT:USDT │ 12 │ 0.72 │ 30.936 │ 3.09 │ 9:55:00 │ 11 0 1 91.7 │
|
||||
│ ETH/USDT:USDT │ 8 │ 0.66 │ 17.864 │ 1.79 │ 1 day, 13:55:00 │ 7 0 1 87.5 │
|
||||
│ XLM/USDT:USDT │ 10 │ 0.31 │ 11.054 │ 1.11 │ 12:08:00 │ 9 0 1 90.0 │
|
||||
│ BTC/USDT:USDT │ 8 │ 0.21 │ 7.289 │ 0.73 │ 3 days, 1:24:00 │ 6 0 2 75.0 │
|
||||
│ XRP/USDT:USDT │ 9 │ -0.14 │ -7.261 │ -0.73 │ 21:18:00 │ 8 0 1 88.9 │
|
||||
│ DOT/USDT:USDT │ 6 │ -0.4 │ -9.187 │ -0.92 │ 5:35:00 │ 4 0 2 66.7 │
|
||||
│ ADA/USDT:USDT │ 8 │ -1.76 │ -52.098 │ -5.21 │ 11:38:00 │ 6 0 2 75.0 │
|
||||
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
└───────────────┴────────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||
LEFT OPEN TRADES REPORT
|
||||
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ BTC/USDT:USDT │ 1 │ -4.14 │ -9.930 │ -0.99 │ 17 days, 8:00:00 │ 0 0 1 0 │
|
||||
│ ETC/USDT:USDT │ 1 │ -4.24 │ -15.365 │ -1.54 │ 10:40:00 │ 0 0 1 0 │
|
||||
│ DOT/USDT:USDT │ 1 │ -5.29 │ -19.125 │ -1.91 │ 11:30:00 │ 0 0 1 0 │
|
||||
│ TOTAL │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||
└───────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────────┴────────────────────────┘
|
||||
ENTER TAG STATS
|
||||
┏━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Enter Tag ┃ Entries ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ OTHER │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
└───────────┴─────────┴──────────────┴─────────────────┴──────────────┴──────────────┴────────────────────────┘
|
||||
EXIT REASON STATS
|
||||
┏━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Exit Reason ┃ Exits ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ roi │ 67 │ 1.05 │ 242.179 │ 24.22 │ 15:49:00 │ 67 0 0 100 │
|
||||
│ exit_signal │ 4 │ -2.23 │ -31.217 │ -3.12 │ 1 day, 8:38:00 │ 0 0 4 0 │
|
||||
│ force_exit │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||
│ stop_loss │ 3 │ -10.14 │ -111.768 │ -11.18 │ 1 day, 3:05:00 │ 0 0 3 0 │
|
||||
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
└─────────────┴───────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||
MIXED TAG STATS
|
||||
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Enter Tag ┃ Exit Reason ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ │ roi │ 67 │ 1.05 │ 242.179 │ 24.22 │ 15:49:00 │ 67 0 0 100 │
|
||||
│ │ exit_signal │ 4 │ -2.23 │ -31.217 │ -3.12 │ 1 day, 8:38:00 │ 0 0 4 0 │
|
||||
│ │ force_exit │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||
│ │ stop_loss │ 3 │ -10.14 │ -111.768 │ -11.18 │ 1 day, 3:05:00 │ 0 0 3 0 │
|
||||
│ TOTAL │ │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
└───────────┴─────────────┴────────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||
SUMMARY METRICS
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Metric ┃ Value ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ Backtesting from │ 2025-07-01 00:00:00 │
|
||||
│ Backtesting to │ 2025-08-01 00:00:00 │
|
||||
│ Trading Mode │ Isolated Futures │
|
||||
│ Max open trades │ 3 │
|
||||
│ │ │
|
||||
│ Total/Daily Avg Trades │ 77 / 2.48 │
|
||||
│ Starting balance │ 1000 USDT │
|
||||
│ Final balance │ 1054.669 USDT │
|
||||
│ Absolute profit │ 54.669 USDT │
|
||||
│ Total profit % │ 5.47% │
|
||||
│ CAGR % │ 87.14% │
|
||||
│ Sortino │ 2.46 │
|
||||
│ Sharpe │ 3.73 │
|
||||
│ Calmar │ 40.81 │
|
||||
│ SQN │ 0.69 │
|
||||
│ Profit factor │ 1.29 │
|
||||
│ Expectancy (Ratio) │ 0.71 (0.04) │
|
||||
│ Avg. daily profit │ 1.764 USDT │
|
||||
│ Avg. stake amount │ 345.251 USDT │
|
||||
│ Total trade volume │ 53352.96 USDT │
|
||||
│ │ │
|
||||
│ Long / Short trades │ 67 / 10 │
|
||||
│ Long / Short profit % │ 8.93% / -3.46% │
|
||||
│ Long / Short profit USDT │ 89.262 / -34.593 │
|
||||
│ │ │
|
||||
│ Best Pair │ LTC/USDT:USDT 5.62% │
|
||||
│ Worst Pair │ ADA/USDT:USDT -5.21% │
|
||||
│ Best trade │ ETC/USDT:USDT 2.00% │
|
||||
│ Worst trade │ ADA/USDT:USDT -10.17% │
|
||||
│ Best day │ 26.931 USDT │
|
||||
│ Worst day │ -47.741 USDT │
|
||||
│ Days win/draw/lose │ 20 / 6 / 5 │
|
||||
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:49 │
|
||||
│ Min/Max/Avg. Duration Losers │ 0d 10:40 / 17d 08:00 / 2d 17:00 │
|
||||
│ Max Consecutive Wins / Loss │ 36 / 3 │
|
||||
│ Rejected Entry signals │ 258 │
|
||||
│ Entry/Exit Timeouts │ 0 / 0 │
|
||||
│ │ │
|
||||
│ Min/Max balance realized │ 1003.168 USDT / 1149.577 USDT │
|
||||
│ Min/Max balance unrealized │ 1000 USDT / 1149.577 USDT │
|
||||
│ Min/Max balance dates │ 2025-07-01 00:05:00 / 2025-07-22 15:15:00 │
|
||||
│ Max % of account underwater │ 8.26% │
|
||||
│ Absolute drawdown │ 94.908 USDT (8.26%) │
|
||||
│ Drawdown duration │ 9 days 08:50:00 │
|
||||
│ Profit at drawdown start │ 149.577 USDT │
|
||||
│ Profit at drawdown end │ 54.669 USDT │
|
||||
│ Drawdown start │ 2025-07-22 15:10:00 │
|
||||
│ Drawdown end │ 2025-08-01 00:00:00 │
|
||||
│ Market change │ 30.51% │
|
||||
└───────────────────────────────┴───────────────────────────────────────────┘
|
||||
BACKTESTING REPORT
|
||||
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ LTC/USDT:USDT │ 16 │ 1.01 │ 56.882 │ 5.69 │ 16:16:00 │ 16 0 0 100 │
|
||||
│ ETC/USDT:USDT │ 12 │ 0.73 │ 31.513 │ 3.15 │ 9:55:00 │ 11 0 1 91.7 │
|
||||
│ ETH/USDT:USDT │ 8 │ 0.69 │ 18.659 │ 1.87 │ 1 day, 13:55:00 │ 7 0 1 87.5 │
|
||||
│ XLM/USDT:USDT │ 10 │ 0.3 │ 10.694 │ 1.07 │ 12:08:00 │ 9 0 1 90.0 │
|
||||
│ BTC/USDT:USDT │ 8 │ 0.22 │ 7.502 │ 0.75 │ 3 days, 1:24:00 │ 6 0 2 75.0 │
|
||||
│ XRP/USDT:USDT │ 9 │ -0.13 │ -6.837 │ -0.68 │ 21:18:00 │ 8 0 1 88.9 │
|
||||
│ DOT/USDT:USDT │ 6 │ -0.39 │ -9.169 │ -0.92 │ 5:35:00 │ 4 0 2 66.7 │
|
||||
│ ADA/USDT:USDT │ 8 │ -1.75 │ -52.089 │ -5.21 │ 11:38:00 │ 6 0 2 75.0 │
|
||||
│ TOTAL │ 77 │ 0.23 │ 57.157 │ 5.72 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
└───────────────┴────────┴──────────────┴─────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||
LEFT OPEN TRADES REPORT
|
||||
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ BTC/USDT:USDT │ 1 │ -4.14 │ -9.930 │ -0.99 │ 17 days, 8:00:00 │ 0 0 1 0 │
|
||||
│ ETC/USDT:USDT │ 1 │ -4.24 │ -15.365 │ -1.54 │ 10:40:00 │ 0 0 1 0 │
|
||||
│ DOT/USDT:USDT │ 1 │ -5.29 │ -19.166 │ -1.92 │ 11:30:00 │ 0 0 1 0 │
|
||||
│ TOTAL │ 3 │ -4.56 │ -44.461 │ -4.45 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||
└───────────────┴────────┴──────────────┴─────────────┴──────────────┴──────────────────┴────────────────────────┘
|
||||
ENTER TAG STATS
|
||||
┏━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Enter Tag ┃ Entries ┃ Avg Profit % ┃ Tot Profit ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ OTHER │ 77 │ 0.23 │ 57.157 │ 5.72 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
│ TOTAL │ 77 │ 0.23 │ 57.157 │ 5.72 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
└───────────┴─────────┴──────────────┴─────────────┴──────────────┴──────────────┴────────────────────────┘
|
||||
EXIT REASON STATS
|
||||
┏━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Exit Reason ┃ Exits ┃ Avg Profit % ┃ Tot Profit ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ roi │ 67 │ 1.06 │ 245.117 │ 24.51 │ 15:49:00 │ 67 0 0 100 │
|
||||
│ exit_signal │ 4 │ -2.23 │ -31.226 │ -3.12 │ 1 day, 8:38:00 │ 0 0 4 0 │
|
||||
│ force_exit │ 3 │ -4.56 │ -44.461 │ -4.45 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||
│ stop_loss │ 3 │ -10.14 │ -112.273 │ -11.23 │ 1 day, 3:05:00 │ 0 0 3 0 │
|
||||
│ TOTAL │ 77 │ 0.23 │ 57.157 │ 5.72 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
└─────────────┴───────┴──────────────┴─────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||
MIXED TAG STATS
|
||||
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Enter Tag ┃ Exit Reason ┃ Trades ┃ Avg Profit % ┃ Tot Profit ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ │ roi │ 67 │ 1.06 │ 245.117 │ 24.51 │ 15:49:00 │ 67 0 0 100 │
|
||||
│ │ exit_signal │ 4 │ -2.23 │ -31.226 │ -3.12 │ 1 day, 8:38:00 │ 0 0 4 0 │
|
||||
│ │ force_exit │ 3 │ -4.56 │ -44.461 │ -4.45 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||
│ │ stop_loss │ 3 │ -10.14 │ -112.273 │ -11.23 │ 1 day, 3:05:00 │ 0 0 3 0 │
|
||||
│ TOTAL │ │ 77 │ 0.23 │ 57.157 │ 5.72 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
└───────────┴─────────────┴────────┴──────────────┴─────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||
SUMMARY METRICS
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Metric ┃ Value ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ Backtesting from │ 2025-07-01 00:00:00 │
|
||||
│ Backtesting to │ 2025-08-01 00:00:00 │
|
||||
│ Trading Mode │ Isolated Futures │
|
||||
│ Max open trades │ 3 │
|
||||
│ │ │
|
||||
│ Total/Daily Avg Trades │ 77 / 2.48 │
|
||||
│ Starting balance │ 1000 USDT │
|
||||
│ Final balance │ 1057.157 USDT │
|
||||
│ Absolute profit │ 57.157 USDT │
|
||||
│ Total profit % │ 5.72% │
|
||||
│ CAGR % │ 92.41% │
|
||||
│ Sharpe (closed trades) │ 3.89 │
|
||||
│ Sortino (closed trades) │ 2.57 │
|
||||
│ Calmar (closed trades) │ 43.03 │
|
||||
│ SQN │ 0.71 │
|
||||
│ Profit factor │ 1.30 │
|
||||
│ Expectancy (Ratio) │ 0.74 (0.04) │
|
||||
│ Avg. daily profit │ 1.844 USDT │
|
||||
│ Avg. stake amount │ 345.478 USDT │
|
||||
│ Market change │ 30.51% │
|
||||
│ Total trade volume │ 53390.788 USDT │
|
||||
│ │ │
|
||||
│ Long / Short trades │ 67 / 10 │
|
||||
│ Long / Short profit % │ 9.19% / -3.48% │
|
||||
│ Long / Short profit USDT │ 91.940 / -34.783 │
|
||||
│ │ │
|
||||
│ Best Pair │ LTC/USDT:USDT 5.69% │
|
||||
│ Worst Pair │ ADA/USDT:USDT -5.21% │
|
||||
│ Best trade │ XRP/USDT:USDT 2.00% │
|
||||
│ Worst trade │ ADA/USDT:USDT -10.17% │
|
||||
│ Best day │ 27.031 USDT │
|
||||
│ Worst day │ -47.826 USDT │
|
||||
│ Days win/draw/lose │ 20 / 6 / 5 │
|
||||
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:49 │
|
||||
│ Min/Max/Avg. Duration Losers │ 0d 10:40 / 17d 08:00 / 2d 17:00 │
|
||||
│ Max Consecutive Wins / Loss │ 36 / 3 │
|
||||
│ Rejected Entry signals │ 258 │
|
||||
│ Entry/Exit Timeouts │ 0 / 0 │
|
||||
│ │ │
|
||||
│ Min/Max balance (closed trades) │ 1003.205 USDT / 1151.425 USDT │
|
||||
│ Max % of account underwater │ 8.19% │
|
||||
│ Absolute drawdown │ 94.268 USDT (8.19%) │
|
||||
│ Drawdown duration │ 9 days 08:50:00 │
|
||||
│ Profit at drawdown start │ 151.425 USDT │
|
||||
│ Profit at drawdown end │ 57.157 USDT │
|
||||
│ Drawdown start │ 2025-07-22 15:10:00 │
|
||||
│ Drawdown end │ 2025-08-01 00:00:00 │
|
||||
│ │ │
|
||||
│ Wallet based Metrics │ │
|
||||
│ Min/Max balance (wallet balance) │ 1000 USDT / 1151.425 USDT │
|
||||
│ Min/Max balance dates (wallet balance) │ 2025-07-01 00:05:00 / 2025-07-22 15:15:00 │
|
||||
│ Max % of account underwater (balance) │ 5.01% │
|
||||
│ Absolute drawdown (wallet balance) │ 54.76 USDT (4.76%) │
|
||||
│ Drawdown duration │ 7 days 20:35:00 │
|
||||
│ Profit at drawdown start │ 151.425 USDT │
|
||||
│ Profit at drawdown end │ 96.664 USDT │
|
||||
│ Drawdown start │ 2025-07-22 15:15:00 │
|
||||
│ Drawdown end │ 2025-07-30 11:50:00 │
|
||||
│ Sharpe (daily wallet balance) │ 4.42 │
|
||||
│ Sortino (daily wallet balance) │ 4.35 │
|
||||
│ Calmar (daily wallet balance) │ 136.07 │
|
||||
└────────────────────────────────────────┴───────────────────────────────────────────┘
|
||||
|
||||
Backtested 2025-07-01 00:00:00 -> 2025-08-01 00:00:00 | Max open trades : 3
|
||||
STRATEGY SUMMARY
|
||||
┏━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Strategy ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃ Drawdown ┃
|
||||
┡━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ SampleStrategy │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │ 94.647 USDT 8.23% │
|
||||
└────────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────┴────────────────────────┴────────────────────┘
|
||||
STRATEGY SUMMARY
|
||||
┏━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
|
||||
┃ Strategy ┃ Trades ┃ Avg Profit % ┃ Tot Profit ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃ Drawdown ┃
|
||||
┡━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
|
||||
│ SampleStrategy │ 77 │ 0.23 │ 57.157 │ 5.72 │ 22:12:00 │ 67 0 10 87.0 │ 94.268 8.19% │
|
||||
└────────────────┴────────┴──────────────┴─────────────┴──────────────┴──────────────┴────────────────────────┴────────────────┘
|
||||
|
||||
```
|
||||
|
||||
### Backtesting report table
|
||||
@@ -330,59 +343,72 @@ The last element of the backtest report is the summary metrics table.
|
||||
It contains key metrics about the performance of your strategy on backtesting data.
|
||||
|
||||
```
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Metric ┃ Value ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ Backtesting from │ 2025-07-01 00:00:00 │
|
||||
│ Backtesting to │ 2025-08-01 00:00:00 │
|
||||
│ Trading Mode │ Isolated Futures │
|
||||
│ Max open trades │ 3 │
|
||||
│ │ │
|
||||
│ Total/Daily Avg Trades │ 77 / 2.48 │
|
||||
│ Starting balance │ 1000 USDT │
|
||||
│ Final balance │ 1054.669 USDT │
|
||||
│ Absolute profit │ 54.669 USDT │
|
||||
│ Total profit % │ 5.47% │
|
||||
│ CAGR % │ 87.14% │
|
||||
│ Sortino │ 2.46 │
|
||||
│ Sharpe │ 3.73 │
|
||||
│ Calmar │ 40.81 │
|
||||
│ SQN │ 0.69 │
|
||||
│ Profit factor │ 1.29 │
|
||||
│ Expectancy (Ratio) │ 0.71 (0.04) │
|
||||
│ Avg. daily profit │ 1.764 USDT │
|
||||
│ Avg. stake amount │ 345.251 USDT │
|
||||
│ Total trade volume │ 53352.96 USDT │
|
||||
│ │ │
|
||||
│ Long / Short trades │ 67 / 10 │
|
||||
│ Long / Short profit % │ 8.93% / -3.46% │
|
||||
│ Long / Short profit USDT │ 89.262 / -34.593 │
|
||||
│ │ │
|
||||
│ Best Pair │ LTC/USDT:USDT 5.62% │
|
||||
│ Worst Pair │ ADA/USDT:USDT -5.21% │
|
||||
│ Best trade │ ETC/USDT:USDT 2.00% │
|
||||
│ Worst trade │ ADA/USDT:USDT -10.17% │
|
||||
│ Best day │ 26.931 USDT │
|
||||
│ Worst day │ -47.741 USDT │
|
||||
│ Days win/draw/lose │ 20 / 6 / 5 │
|
||||
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:49 │
|
||||
│ Min/Max/Avg. Duration Losers │ 0d 10:40 / 17d 08:00 / 2d 17:00 │
|
||||
│ Max Consecutive Wins / Loss │ 36 / 3 │
|
||||
│ Rejected Entry signals │ 258 │
|
||||
│ Entry/Exit Timeouts │ 0 / 0 │
|
||||
│ │ │
|
||||
│ Min/Max balance realized │ 1003.168 USDT / 1149.577 USDT │
|
||||
│ Min/Max balance unrealized │ 1000 USDT / 1149.577 USDT │
|
||||
│ Min/Max balance dates │ 2025-07-01 00:05:00 / 2025-07-22 15:15:00 │
|
||||
│ Max % of account underwater │ 8.26% │
|
||||
│ Absolute drawdown │ 94.908 USDT (8.26%) │
|
||||
│ Drawdown duration │ 9 days 08:50:00 │
|
||||
│ Profit at drawdown start │ 149.577 USDT │
|
||||
│ Profit at drawdown end │ 54.669 USDT │
|
||||
│ Drawdown start │ 2025-07-22 15:10:00 │
|
||||
│ Drawdown end │ 2025-08-01 00:00:00 │
|
||||
│ Market change │ 30.51% │
|
||||
└───────────────────────────────┴───────────────────────────────────────────┘
|
||||
SUMMARY METRICS
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Metric ┃ Value ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ Backtesting from │ 2025-07-01 00:00:00 │
|
||||
│ Backtesting to │ 2025-08-01 00:00:00 │
|
||||
│ Trading Mode │ Isolated Futures │
|
||||
│ Max open trades │ 3 │
|
||||
│ │ │
|
||||
│ Total/Daily Avg Trades │ 77 / 2.48 │
|
||||
│ Starting balance │ 1000 USDT │
|
||||
│ Final balance │ 1057.157 USDT │
|
||||
│ Absolute profit │ 57.157 USDT │
|
||||
│ Total profit % │ 5.72% │
|
||||
│ CAGR % │ 92.41% │
|
||||
│ Sharpe (closed trades) │ 3.89 │
|
||||
│ Sortino (closed trades) │ 2.57 │
|
||||
│ Calmar (closed trades) │ 43.03 │
|
||||
│ SQN │ 0.71 │
|
||||
│ Profit factor │ 1.30 │
|
||||
│ Expectancy (Ratio) │ 0.74 (0.04) │
|
||||
│ Avg. daily profit │ 1.844 USDT │
|
||||
│ Avg. stake amount │ 345.478 USDT │
|
||||
│ Market change │ 30.51% │
|
||||
│ Total trade volume │ 53390.788 USDT │
|
||||
│ │ │
|
||||
│ Long / Short trades │ 67 / 10 │
|
||||
│ Long / Short profit % │ 9.19% / -3.48% │
|
||||
│ Long / Short profit USDT │ 91.940 / -34.783 │
|
||||
│ │ │
|
||||
│ Best Pair │ LTC/USDT:USDT 5.69% │
|
||||
│ Worst Pair │ ADA/USDT:USDT -5.21% │
|
||||
│ Best trade │ XRP/USDT:USDT 2.00% │
|
||||
│ Worst trade │ ADA/USDT:USDT -10.17% │
|
||||
│ Best day │ 27.031 USDT │
|
||||
│ Worst day │ -47.826 USDT │
|
||||
│ Days win/draw/lose │ 20 / 6 / 5 │
|
||||
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:49 │
|
||||
│ Min/Max/Avg. Duration Losers │ 0d 10:40 / 17d 08:00 / 2d 17:00 │
|
||||
│ Max Consecutive Wins / Loss │ 36 / 3 │
|
||||
│ Rejected Entry signals │ 258 │
|
||||
│ Entry/Exit Timeouts │ 0 / 0 │
|
||||
│ │ │
|
||||
│ Min/Max balance (closed trades) │ 1003.205 USDT / 1151.425 USDT │
|
||||
│ Max % of account underwater │ 8.19% │
|
||||
│ Absolute drawdown │ 94.268 USDT (8.19%) │
|
||||
│ Drawdown duration │ 9 days 08:50:00 │
|
||||
│ Profit at drawdown start │ 151.425 USDT │
|
||||
│ Profit at drawdown end │ 57.157 USDT │
|
||||
│ Drawdown start │ 2025-07-22 15:10:00 │
|
||||
│ Drawdown end │ 2025-08-01 00:00:00 │
|
||||
│ │ │
|
||||
│ Wallet based Metrics │ │
|
||||
│ Min/Max balance (wallet balance) │ 1000 USDT / 1151.425 USDT │
|
||||
│ Min/Max balance dates (wallet balance) │ 2025-07-01 00:05:00 / 2025-07-22 15:15:00 │
|
||||
│ Max % of account underwater (balance) │ 5.01% │
|
||||
│ Absolute drawdown (wallet balance) │ 54.76 USDT (4.76%) │
|
||||
│ Drawdown duration │ 7 days 20:35:00 │
|
||||
│ Profit at drawdown start │ 151.425 USDT │
|
||||
│ Profit at drawdown end │ 96.664 USDT │
|
||||
│ Drawdown start │ 2025-07-22 15:15:00 │
|
||||
│ Drawdown end │ 2025-07-30 11:50:00 │
|
||||
│ Sharpe (daily wallet balance) │ 4.42 │
|
||||
│ Sortino (daily wallet balance) │ 4.35 │
|
||||
│ Calmar (daily wallet balance) │ 136.07 │
|
||||
└────────────────────────────────────────┴───────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
|
||||
@@ -394,14 +420,15 @@ It contains key metrics about the performance of your strategy on backtesting da
|
||||
- `Absolute profit`: Profit made in stake currency.
|
||||
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`.
|
||||
- `CAGR %`: Compound annual growth rate.
|
||||
- `Sortino`: Annualized Sortino ratio.
|
||||
- `Sharpe`: Annualized Sharpe ratio.
|
||||
- `Calmar`: Annualized Calmar ratio.
|
||||
- `Sharpe (closed trades)`: Annualized Sharpe ratio including only closed trades (ignoring open trades with profits or losses).
|
||||
- `Sortino (closed trades)`: Annualized Sortino ratio including only closed trades (ignoring open trades with profits or losses).
|
||||
- `Calmar (closed trades)`: Annualized Calmar ratio including only closed trades (ignoring open trades with profits or losses).
|
||||
- `SQN`: System Quality Number (SQN) - by Van Tharp.
|
||||
- `Profit factor`: Sum of the profits of all winning trades divided by the sum of the losses of all losing trades.
|
||||
- `Expectancy (Ratio)`: Expectancy ratio, which is the average profit or loss per trade. A negative expectancy ratio means that your strategy is not profitable.
|
||||
- `Avg. daily profit`: Average profit per day, calculated as `(Total Profit / Backtest Days)`.
|
||||
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
||||
- `Market change`: Change of the market during the backtest period. Calculated as the average of all pairs' changes from the first to the last candle using the "close" column.
|
||||
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||
- `Long / Short trades`: Split long/short trade counts (only shown when short trades were made).
|
||||
- `Long / Short profit %`: Profit percentage for long and short trades (only shown when short trades were made).
|
||||
@@ -415,15 +442,21 @@ It contains key metrics about the performance of your strategy on backtesting da
|
||||
- `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row.
|
||||
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
|
||||
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
||||
- `Min/Max balance realized`: Lowest and Highest Wallet balance during the backtest period based on closed trades trades.
|
||||
- `Min/Max balance unrealized`: Lowest and Highest Wallet balance during the backtest period - including capital tied in open trades.
|
||||
- `Min/Max balance dates`: Dates when the minimum and maximum unrealized balance occurred.
|
||||
- `Min/Max balance (closed trades)`: Lowest and Highest Wallet balance during the backtest period based on closed trades trades.
|
||||
- `Max % of account underwater`: Maximum percentage your account has decreased from the top since the simulation started. Calculated as the maximum of `(Max Balance - Current Balance) / (Max Balance)`.
|
||||
- `Absolute drawdown`: Maximum absolute drawdown experienced, including percentage relative to the account calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`..
|
||||
- `Absolute drawdown (wallet balance)`: Maximum absolute drawdown experienced based on the unrealized balance, including percentage relative to the account calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
||||
- `Drawdown duration`: Duration of the largest drawdown period.
|
||||
- `Profit at drawdown start` / `Profit at drawdown end`: Profit at the beginning and end of the largest drawdown period.
|
||||
- `Drawdown start` / `Drawdown end`: Start and end datetime for the largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
||||
- `Market change`: Change of the market during the backtest period. Calculated as the average of all pairs' changes from the first to the last candle using the "close" column.
|
||||
- `Min/Max balance (wallet balance)`: Lowest and Highest Wallet balance during the backtest period - including capital tied in open trades.
|
||||
- `Min/Max balance dates (wallet balance)`: Dates when the minimum and maximum unrealized balance occurred.
|
||||
- `Sharpe (wallet balance)` Annualized Sharpe ratio calculation including unrealized profits.
|
||||
- `Sortino (wallet balance)` Annualized Sortino ratio calculation including unrealized profits.
|
||||
- `Calmar (wallet balance)` Annualized Calmar ratio calculation including unrealized profits.
|
||||
|
||||
!!! Tip "Wallet based Metrics"
|
||||
The metrics under the "Wallet based Metrics" section are calculated based on the unrealized balance, which includes the capital tied in open trades. This provides a more comprehensive view of the strategy's performance, as it accounts for both realized and unrealized profits and losses.
|
||||
|
||||
### Daily / Weekly / Monthly / Yearly breakdown
|
||||
|
||||
|
||||
+4
-4
@@ -111,10 +111,10 @@ It also allows multiple subplots to display both MACD and RSI at the same time.
|
||||
|
||||
Plot type can be configured using `type` key. Possible types are:
|
||||
|
||||
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
|
||||
* `bar` corresponding to `plotly.graph_objects.Bar` class.
|
||||
* `scatter` corresponding a scatter plot.
|
||||
* `bar` corresponding to a bar plot.
|
||||
|
||||
Extra parameters to `plotly.graph_objects.*` constructor can be specified in `plotly` dict.
|
||||
Extra parameters to `plotly.graph_objects.*` constructor can be specified in `plotly` dict - these are only supported when using plotly as plotting library and will be ignored when using freq-ui.
|
||||
|
||||
Sample configuration with inline comments explaining the process:
|
||||
|
||||
@@ -163,7 +163,7 @@ def plot_config(self):
|
||||
```
|
||||
|
||||
??? Note "As attribute (former method)"
|
||||
Assigning plot_config is also possible as Attribute (this used to be the default way).
|
||||
Assigning `plot_config` is also possible as Attribute (this used to be the default way).
|
||||
This has the disadvantage that strategy parameters are not available, preventing certain configurations from working.
|
||||
|
||||
``` python
|
||||
|
||||
@@ -10,7 +10,6 @@ from io import BytesIO, StringIO
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||
@@ -308,7 +307,7 @@ def get_backtest_market_change(filename: Path, include_ts: bool = True) -> pd.Da
|
||||
else:
|
||||
df = pd.read_feather(filename)
|
||||
if include_ts:
|
||||
df.loc[:, "__date_ts"] = df.loc[:, "date"].astype(np.int64) // 1000 // 1000
|
||||
df.loc[:, "__date_ts"] = df.loc[:, "date"].dt.as_unit("ms").astype("int64")
|
||||
return df
|
||||
|
||||
|
||||
@@ -326,7 +325,7 @@ def get_backtest_wallet_change(filename: Path, strategy_name: str) -> pd.DataFra
|
||||
data = load_file_from_zip(filename, f"{filename.stem}_{strategy_name}_wallet.feather")
|
||||
df = pd.read_feather(BytesIO(data))
|
||||
|
||||
df.loc[:, "__date_ts"] = df.loc[:, "date"].astype(np.int64) // 1000 // 1000
|
||||
df.loc[:, "__date_ts"] = df.loc[:, "date"].dt.as_unit("ms").astype("int64")
|
||||
return df
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
from pandas import DataFrame, read_json, to_datetime
|
||||
|
||||
from freqtrade import misc
|
||||
@@ -35,8 +34,8 @@ class JsonDataHandler(IDataHandler):
|
||||
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
|
||||
self.create_dir_if_needed(filename)
|
||||
_data = data.copy()
|
||||
# Convert date to int
|
||||
_data["date"] = _data["date"].astype(np.int64) // 1000 // 1000
|
||||
# Convert date to int (milliseconds)
|
||||
_data["date"] = _data["date"].dt.as_unit("ms").astype("int64")
|
||||
|
||||
# Reset index, select only appropriate columns and save as json
|
||||
_data.reset_index(drop=True).loc[:, self._columns].to_json(
|
||||
|
||||
+195
-24
@@ -333,6 +333,72 @@ def calculate_expectancy(trades: pd.DataFrame) -> tuple[float, float]:
|
||||
return expectancy, expectancy_ratio
|
||||
|
||||
|
||||
def _calculate_annualized_ratio(
|
||||
expected_returns_mean: float,
|
||||
denominator: float,
|
||||
annualization_factor: int = 365,
|
||||
) -> float:
|
||||
"""
|
||||
Helper function to calculate annualized ratios like Sharpe and Sortino.
|
||||
:param expected_returns_mean: Mean of the returns (expected returns)
|
||||
:param denominator: Denominator of the ratio (e.g. standard deviation for Sharpe)
|
||||
:param annualization_factor: Factor to annualize the ratio (default is 365 for daily returns)
|
||||
:return: Annualized ratio, or -100.0 if denominator is zero or NaN to indicate this is
|
||||
not optimal.
|
||||
"""
|
||||
if denominator != 0 and not np.isnan(denominator):
|
||||
return float(expected_returns_mean / denominator * np.sqrt(annualization_factor))
|
||||
|
||||
# Define high (negative) ratio to be clear that this is NOT optimal.
|
||||
return -100.0
|
||||
|
||||
|
||||
def _calculate_daily_returns_from_balance(
|
||||
balance_history: pd.DataFrame,
|
||||
date_col: str,
|
||||
balance_col: str,
|
||||
) -> pd.Series:
|
||||
wallet = _prepare_balance_history(balance_history, date_col, balance_col)
|
||||
if len(wallet) == 0:
|
||||
return pd.DataFrame(columns=[date_col, balance_col])
|
||||
|
||||
# Sample balance to daily end-of-day values to normalize variable snapshot frequency.
|
||||
daily_balance = (
|
||||
wallet.set_index(date_col)[balance_col].resample("1D").last().dropna().rename(balance_col)
|
||||
)
|
||||
daily_balance = daily_balance.reset_index()
|
||||
|
||||
if len(daily_balance) < 2:
|
||||
return pd.Series(dtype=float)
|
||||
|
||||
return daily_balance[balance_col].pct_change().dropna()
|
||||
|
||||
|
||||
def _prepare_balance_history(
|
||||
balance_history: pd.DataFrame,
|
||||
date_col: str,
|
||||
balance_col: str,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Prepare balance history for calculations by filtering out rows with
|
||||
missing date or balance values.
|
||||
"""
|
||||
if (
|
||||
len(balance_history) == 0
|
||||
or date_col not in balance_history
|
||||
or balance_col not in balance_history
|
||||
):
|
||||
return pd.DataFrame(columns=[date_col, balance_col])
|
||||
|
||||
wallet = balance_history.loc[:, [date_col, balance_col]].copy()
|
||||
wallet = wallet.dropna(subset=[date_col, balance_col]).sort_values(date_col)
|
||||
|
||||
if len(wallet) == 0:
|
||||
return pd.DataFrame(columns=[date_col, balance_col])
|
||||
|
||||
return wallet
|
||||
|
||||
|
||||
def calculate_sortino(
|
||||
trades: pd.DataFrame,
|
||||
min_date: datetime | None,
|
||||
@@ -354,14 +420,31 @@ def calculate_sortino(
|
||||
|
||||
down_stdev = np.std(trades.loc[trades["profit_abs"] < 0, "profit_abs"] / starting_balance)
|
||||
|
||||
if down_stdev != 0 and not np.isnan(down_stdev):
|
||||
sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
|
||||
else:
|
||||
# Define high (negative) sortino ratio to be clear that this is NOT optimal.
|
||||
sortino_ratio = -100
|
||||
return _calculate_annualized_ratio(expected_returns_mean, down_stdev)
|
||||
|
||||
# print(expected_returns_mean, down_stdev, sortino_ratio)
|
||||
return sortino_ratio
|
||||
|
||||
def calculate_sortino_from_balance(
|
||||
balance_history: pd.DataFrame,
|
||||
date_col: str = "date",
|
||||
balance_col: str = "total_quote",
|
||||
) -> float:
|
||||
"""
|
||||
Calculate sortino ratio from historical balance snapshots.
|
||||
|
||||
:param balance_history: DataFrame containing at least date and balance columns
|
||||
:param date_col: Column containing timestamps
|
||||
:param balance_col: Column containing historical balance values
|
||||
:return: sortino
|
||||
"""
|
||||
daily_returns = _calculate_daily_returns_from_balance(balance_history, date_col, balance_col)
|
||||
|
||||
if len(daily_returns) == 0:
|
||||
return 0.0
|
||||
|
||||
expected_returns_mean = daily_returns.mean()
|
||||
downside_returns = daily_returns[daily_returns < 0]
|
||||
down_stdev = downside_returns.std(ddof=0)
|
||||
return _calculate_annualized_ratio(expected_returns_mean, down_stdev)
|
||||
|
||||
|
||||
def calculate_sharpe(
|
||||
@@ -384,14 +467,67 @@ def calculate_sharpe(
|
||||
expected_returns_mean = total_profit.sum() / days_period
|
||||
up_stdev = np.std(total_profit)
|
||||
|
||||
if up_stdev != 0:
|
||||
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
|
||||
else:
|
||||
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||
sharp_ratio = -100
|
||||
return _calculate_annualized_ratio(expected_returns_mean, up_stdev)
|
||||
|
||||
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
||||
return sharp_ratio
|
||||
|
||||
def calculate_sharpe_from_balance(
|
||||
balance_history: pd.DataFrame,
|
||||
date_col: str = "date",
|
||||
balance_col: str = "total_quote",
|
||||
) -> float:
|
||||
"""
|
||||
Calculate sharpe ratio from historical balance snapshots.
|
||||
|
||||
:param balance_history: DataFrame containing at least date and balance columns
|
||||
:param date_col: Column containing timestamps
|
||||
:param balance_col: Column containing historical balance values
|
||||
:return: sharpe
|
||||
"""
|
||||
daily_returns = _calculate_daily_returns_from_balance(balance_history, date_col, balance_col)
|
||||
|
||||
if len(daily_returns) == 0:
|
||||
return 0.0
|
||||
|
||||
expected_returns_mean = daily_returns.mean()
|
||||
up_stdev = daily_returns.std(ddof=0)
|
||||
return _calculate_annualized_ratio(expected_returns_mean, up_stdev)
|
||||
|
||||
|
||||
def calculate_max_drawdown_from_balance(
|
||||
balance_history: pd.DataFrame,
|
||||
date_col: str = "date",
|
||||
balance_col: str = "total_quote",
|
||||
relative: bool = False,
|
||||
) -> DrawDownResult:
|
||||
"""
|
||||
Calculate max drawdown from historical balance snapshots.
|
||||
|
||||
:param balance_history: DataFrame containing at least date and balance columns
|
||||
:param date_col: Column containing timestamps
|
||||
:param balance_col: Column containing historical balance values
|
||||
:param relative: If True, use relative drawdown for max calculation instead of absolute
|
||||
:return: DrawDownResult object
|
||||
:raise: ValueError if balance-history dataframe was found empty.
|
||||
"""
|
||||
wallet = _prepare_balance_history(
|
||||
balance_history=balance_history,
|
||||
date_col=date_col,
|
||||
balance_col=balance_col,
|
||||
)
|
||||
|
||||
if len(wallet) < 2:
|
||||
raise ValueError("Balance-history dataframe empty.")
|
||||
|
||||
starting_balance = float(wallet[balance_col].iloc[0])
|
||||
wallet.loc[:, "total_balance"] = wallet[balance_col].diff().fillna(0.0)
|
||||
|
||||
return calculate_max_drawdown(
|
||||
wallet,
|
||||
date_col=date_col,
|
||||
value_col="total_balance",
|
||||
starting_balance=starting_balance,
|
||||
relative=relative,
|
||||
)
|
||||
|
||||
|
||||
def calculate_calmar(
|
||||
@@ -401,12 +537,12 @@ def calculate_calmar(
|
||||
starting_balance: float,
|
||||
) -> float:
|
||||
"""
|
||||
Calculate calmar
|
||||
Calculate calmar from trades data.
|
||||
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||
:return: calmar
|
||||
"""
|
||||
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||
return 0
|
||||
return 0.0
|
||||
|
||||
total_profit = trades["profit_abs"].sum() / starting_balance
|
||||
days_period = max(1, (max_date - min_date).days)
|
||||
@@ -422,16 +558,51 @@ def calculate_calmar(
|
||||
)
|
||||
max_drawdown = drawdown.relative_account_drawdown
|
||||
except ValueError:
|
||||
max_drawdown = 0
|
||||
return 0.0
|
||||
|
||||
if max_drawdown != 0:
|
||||
calmar_ratio = expected_returns_mean / max_drawdown * math.sqrt(365)
|
||||
else:
|
||||
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
|
||||
calmar_ratio = -100
|
||||
return _calculate_annualized_ratio(expected_returns_mean, max_drawdown)
|
||||
|
||||
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||
return calmar_ratio
|
||||
|
||||
def calculate_calmar_from_balance(
|
||||
balance_history: pd.DataFrame,
|
||||
date_col: str = "date",
|
||||
balance_col: str = "total_quote",
|
||||
) -> float:
|
||||
"""
|
||||
Calculate calmar ratio from historical balance snapshots.
|
||||
|
||||
:param balance_history: DataFrame containing at least date and balance columns
|
||||
:param date_col: Column containing timestamps
|
||||
:param balance_col: Column containing historical balance values
|
||||
:return: calmar
|
||||
"""
|
||||
wallet = _prepare_balance_history(
|
||||
balance_history=balance_history,
|
||||
date_col=date_col,
|
||||
balance_col=balance_col,
|
||||
)
|
||||
|
||||
if len(wallet) < 2:
|
||||
return 0.0
|
||||
|
||||
starting_balance = float(wallet[balance_col].iloc[0])
|
||||
final_balance = float(wallet[balance_col].iloc[-1])
|
||||
days_period = max(1, (wallet[date_col].iloc[-1] - wallet[date_col].iloc[0]).days)
|
||||
|
||||
total_profit = (final_balance - starting_balance) / starting_balance
|
||||
expected_returns_mean = total_profit / days_period * 100
|
||||
|
||||
try:
|
||||
drawdown = calculate_max_drawdown_from_balance(
|
||||
wallet,
|
||||
date_col=date_col,
|
||||
balance_col=balance_col,
|
||||
)
|
||||
max_drawdown = drawdown.relative_account_drawdown
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
return _calculate_annualized_ratio(expected_returns_mean, max_drawdown)
|
||||
|
||||
|
||||
def calculate_sqn(trades: pd.DataFrame, starting_balance: float) -> float:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -101,30 +101,36 @@ class Bitget(Exchange):
|
||||
return order
|
||||
|
||||
def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> CcxtOrder:
|
||||
params2 = {
|
||||
"stop": True,
|
||||
}
|
||||
for method in (
|
||||
self._api.fetch_open_orders,
|
||||
self._api.fetch_canceled_and_closed_orders,
|
||||
):
|
||||
try:
|
||||
orders = method(pair, params=params2)
|
||||
orders_f = [order for order in orders if order["id"] == order_id]
|
||||
if orders_f:
|
||||
order = orders_f[0]
|
||||
self._log_exchange_response("get_stop_order_fallback", order)
|
||||
return self._convert_stop_order(pair, order_id, order)
|
||||
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
|
||||
pass
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
|
||||
) from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
# old stoploss orders
|
||||
paramsold = {"stop": True}
|
||||
# new stoploss orders with stopLossPrice (used in futures starting 2026.4)
|
||||
paramsnew = {"planType": "profit_loss"}
|
||||
params_to_try = (
|
||||
(paramsnew, paramsold) if self.trading_mode == TradingMode.FUTURES else (paramsold,)
|
||||
)
|
||||
|
||||
for params2 in params_to_try:
|
||||
for method in (
|
||||
self._api.fetch_open_orders,
|
||||
self._api.fetch_canceled_and_closed_orders,
|
||||
):
|
||||
try:
|
||||
orders = method(pair, params=params2)
|
||||
orders_f = [order for order in orders if order["id"] == order_id]
|
||||
if orders_f:
|
||||
order = orders_f[0]
|
||||
self._log_exchange_response("get_stop_order_fallback", order)
|
||||
return self._convert_stop_order(pair, order_id, order)
|
||||
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
|
||||
pass
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
|
||||
) from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
|
||||
|
||||
@retrier(retries=API_RETRY_COUNT)
|
||||
|
||||
@@ -361,7 +361,7 @@ class FreqaiDataDrawer:
|
||||
label_loc = df.columns.get_loc(label)
|
||||
pred_label_loc = predictions.columns.get_loc(label)
|
||||
df.iloc[-1, label_loc] = predictions.iloc[-1, pred_label_loc]
|
||||
if df[label].dtype == object:
|
||||
if pd.api.types.is_string_dtype(df[label].dtype):
|
||||
continue
|
||||
label_mean_loc = df.columns.get_loc(f"{label}_mean")
|
||||
label_std_loc = df.columns.get_loc(f"{label}_std")
|
||||
|
||||
@@ -435,7 +435,7 @@ class FreqaiDataKitchen:
|
||||
|
||||
for label in predictions.columns:
|
||||
append_dict[label] = predictions[label]
|
||||
if predictions[label].dtype == object:
|
||||
if pd.api.types.is_string_dtype(predictions[label].dtype):
|
||||
continue
|
||||
if "labels_mean" in self.data and label in self.data["labels_mean"]:
|
||||
append_dict[f"{label}_mean"] = self.data["labels_mean"][label]
|
||||
@@ -879,7 +879,7 @@ class FreqaiDataKitchen:
|
||||
|
||||
self.data["labels_mean"], self.data["labels_std"] = {}, {}
|
||||
for label in self.data_dictionary["train_labels"].columns:
|
||||
if self.data_dictionary["train_labels"][label].dtype == object:
|
||||
if pd.api.types.is_string_dtype(self.data_dictionary["train_labels"][label].dtype):
|
||||
continue
|
||||
f = spy.stats.norm.fit(self.data_dictionary["train_labels"][label])
|
||||
self.data["labels_mean"][label], self.data["labels_std"][label] = f[0], f[1]
|
||||
@@ -905,7 +905,7 @@ class FreqaiDataKitchen:
|
||||
self.find_labels(dataframe)
|
||||
|
||||
for key in self.label_list:
|
||||
if dataframe[key].dtype == object:
|
||||
if pd.api.types.is_string_dtype(dataframe[key].dtype):
|
||||
self.unique_classes[key] = dataframe[key].dropna().unique()
|
||||
|
||||
if self.unique_classes:
|
||||
|
||||
@@ -676,7 +676,7 @@ class IFreqaiModel(ABC):
|
||||
self.set_start_dry_live_date(strat_df)
|
||||
|
||||
for label in hist_preds_df.columns:
|
||||
if hist_preds_df[label].dtype == object:
|
||||
if pd.api.types.is_string_dtype(hist_preds_df[label].dtype):
|
||||
continue
|
||||
hist_preds_df[f"{label}_mean"] = 0
|
||||
hist_preds_df[f"{label}_std"] = 0
|
||||
@@ -706,7 +706,7 @@ class IFreqaiModel(ABC):
|
||||
num_candles = self.freqai_info.get("fit_live_predictions_candles", 100)
|
||||
dk.data["labels_mean"], dk.data["labels_std"] = {}, {}
|
||||
for label in full_labels:
|
||||
if self.dd.historic_predictions[dk.pair][label].dtype == object:
|
||||
if pd.api.types.is_string_dtype(self.dd.historic_predictions[dk.pair][label].dtype):
|
||||
continue
|
||||
f = spy.stats.norm.fit(self.dd.historic_predictions[dk.pair][label].tail(num_candles))
|
||||
dk.data["labels_mean"][label], dk.data["labels_std"][label] = f[0], f[1]
|
||||
@@ -896,7 +896,7 @@ class IFreqaiModel(ABC):
|
||||
]
|
||||
self.fit_live_predictions(self.dk, self.dk.pair)
|
||||
for label in label_columns:
|
||||
if dk.full_df[label].dtype == object:
|
||||
if pd.api.types.is_string_dtype(dk.full_df[label].dtype):
|
||||
continue
|
||||
if "labels_mean" in self.dk.data:
|
||||
dk.full_df.at[index, f"{label}_mean"] = self.dk.data["labels_mean"][
|
||||
|
||||
@@ -126,14 +126,14 @@ class LookaheadAnalysisSubFunctions:
|
||||
csv_df = add_or_update_row(csv_df, new_row_data)
|
||||
|
||||
# Fill NaN values with a default value (e.g., 0)
|
||||
csv_df["total_signals"] = csv_df["total_signals"].astype(int).fillna(0)
|
||||
csv_df["biased_entry_signals"] = csv_df["biased_entry_signals"].astype(int).fillna(0)
|
||||
csv_df["biased_exit_signals"] = csv_df["biased_exit_signals"].astype(int).fillna(0)
|
||||
csv_df["total_signals"] = csv_df["total_signals"].astype("int64").fillna(0)
|
||||
csv_df["biased_entry_signals"] = csv_df["biased_entry_signals"].astype("int64").fillna(0)
|
||||
csv_df["biased_exit_signals"] = csv_df["biased_exit_signals"].astype("int64").fillna(0)
|
||||
|
||||
# Convert columns to integers
|
||||
csv_df["total_signals"] = csv_df["total_signals"].astype(int)
|
||||
csv_df["biased_entry_signals"] = csv_df["biased_entry_signals"].astype(int)
|
||||
csv_df["biased_exit_signals"] = csv_df["biased_exit_signals"].astype(int)
|
||||
csv_df["total_signals"] = csv_df["total_signals"].astype("int64")
|
||||
csv_df["biased_entry_signals"] = csv_df["biased_entry_signals"].astype("int64")
|
||||
csv_df["biased_exit_signals"] = csv_df["biased_exit_signals"].astype("int64")
|
||||
|
||||
logger.info(f"saving {config['lookahead_analysis_exportfilename']}")
|
||||
csv_df.to_csv(config["lookahead_analysis_exportfilename"], index=False)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
from typing import Any, Literal
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config
|
||||
from freqtrade.ft_types import BacktestResultType
|
||||
from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats
|
||||
@@ -9,6 +11,8 @@ from freqtrade.util import decimals_per_coin, fmt_coin, print_rich_table
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__EMPTY_LINE = ("", "")
|
||||
|
||||
|
||||
def _get_line_floatfmt(stake_currency: str) -> list[str]:
|
||||
"""
|
||||
@@ -201,7 +205,7 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
||||
|
||||
short_metrics = (
|
||||
[
|
||||
("", ""), # Empty line to improve readability
|
||||
__EMPTY_LINE, # Empty line to improve readability
|
||||
(
|
||||
"Long / Short trades",
|
||||
f"{strat_results.get('trade_count_long', 'total_trades')} / "
|
||||
@@ -222,7 +226,7 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
||||
else []
|
||||
)
|
||||
|
||||
drawdown_metrics = []
|
||||
drawdown_metrics: list[tuple[str | Text, str | Text]] = []
|
||||
if "max_relative_drawdown" in strat_results:
|
||||
# Compatibility to show old hyperopt results
|
||||
drawdown_metrics.append(
|
||||
@@ -289,25 +293,77 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
||||
)
|
||||
wallet_metrics: list[tuple[str, str]] = [
|
||||
(
|
||||
"Min/Max balance realized",
|
||||
"Min/Max balance (closed trades)",
|
||||
f"{fmt_coin(strat_results['csum_min'], stake)} / "
|
||||
f"{fmt_coin(strat_results['csum_max'], stake)}",
|
||||
),
|
||||
]
|
||||
if wallet_stats := strat_results.get("wallet_stats"):
|
||||
wallet_metrics.extend(
|
||||
wallet_stats = strat_results.get("wallet_stats", {})
|
||||
if wallet_stats:
|
||||
drawdown_metrics.extend(
|
||||
[
|
||||
__EMPTY_LINE, # Empty line to improve readability
|
||||
(Text("Wallet based Metrics", style="bold"), ""),
|
||||
(
|
||||
"Min/Max balance unrealized",
|
||||
"Min/Max balance (wallet balance)",
|
||||
f"{fmt_coin(wallet_stats['low_balance'], stake)} / "
|
||||
f"{fmt_coin(wallet_stats['high_balance'], stake)}",
|
||||
),
|
||||
(
|
||||
"Min/Max balance dates",
|
||||
"Min/Max balance dates (wallet balance)",
|
||||
f"{wallet_stats['low_date']} / {wallet_stats['high_date']}",
|
||||
),
|
||||
]
|
||||
)
|
||||
if "max_drawdown_abs" in wallet_stats:
|
||||
# Assume that if sharpe is there, all others are there as well.
|
||||
drawdown_metrics.extend(
|
||||
[
|
||||
(
|
||||
"Max % of account underwater (balance)",
|
||||
f"{wallet_stats['max_relative_drawdown']:.2%}",
|
||||
),
|
||||
(
|
||||
"Absolute drawdown (wallet balance)",
|
||||
f"{fmt_coin(wallet_stats['max_drawdown_abs'], stake)} "
|
||||
f"({wallet_stats['max_drawdown_account']:.2%})",
|
||||
),
|
||||
(
|
||||
"Drawdown duration",
|
||||
wallet_stats["drawdown_duration"]
|
||||
if "drawdown_duration" in wallet_stats
|
||||
else "N/A",
|
||||
),
|
||||
(
|
||||
"Profit at drawdown start",
|
||||
fmt_coin(wallet_stats["max_drawdown_high"], stake),
|
||||
),
|
||||
(
|
||||
"Profit at drawdown end",
|
||||
fmt_coin(wallet_stats["max_drawdown_low"], stake),
|
||||
),
|
||||
("Drawdown start", wallet_stats["drawdown_start"]),
|
||||
("Drawdown end", wallet_stats["drawdown_end"]),
|
||||
(
|
||||
"Sharpe (daily wallet balance)",
|
||||
f"{wallet_stats['sharpe']:.2f}"
|
||||
if wallet_stats and "sharpe" in wallet_stats
|
||||
else "N/A",
|
||||
),
|
||||
(
|
||||
"Sortino (daily wallet balance)",
|
||||
f"{wallet_stats['sortino']:.2f}"
|
||||
if wallet_stats and "sortino" in wallet_stats
|
||||
else "N/A",
|
||||
),
|
||||
(
|
||||
"Calmar (daily wallet balance)",
|
||||
f"{wallet_stats['calmar']:.2f}"
|
||||
if wallet_stats and "calmar" in wallet_stats
|
||||
else "N/A",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
|
||||
# command stores these results and newer version of freqtrade must be able to handle old
|
||||
@@ -317,7 +373,7 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
||||
("Backtesting to", strat_results["backtest_end"]),
|
||||
*trading_mode,
|
||||
("Max open trades", strat_results["max_open_trades"]),
|
||||
("", ""), # Empty line to improve readability
|
||||
__EMPTY_LINE, # Empty line to improve readability
|
||||
(
|
||||
"Total/Daily Avg Trades",
|
||||
f"{strat_results['total_trades']} / {strat_results['trades_per_day']}",
|
||||
@@ -336,9 +392,18 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
||||
),
|
||||
("Total profit %", f"{strat_results['profit_total']:.2%}"),
|
||||
("CAGR %", f"{strat_results['cagr']:.2%}" if "cagr" in strat_results else "N/A"),
|
||||
("Sortino", f"{strat_results['sortino']:.2f}" if "sortino" in strat_results else "N/A"),
|
||||
("Sharpe", f"{strat_results['sharpe']:.2f}" if "sharpe" in strat_results else "N/A"),
|
||||
("Calmar", f"{strat_results['calmar']:.2f}" if "calmar" in strat_results else "N/A"),
|
||||
(
|
||||
"Sharpe (closed trades)",
|
||||
f"{strat_results['sharpe']:.2f}" if "sharpe" in strat_results else "N/A",
|
||||
),
|
||||
(
|
||||
"Sortino (closed trades)",
|
||||
f"{strat_results['sortino']:.2f}" if "sortino" in strat_results else "N/A",
|
||||
),
|
||||
(
|
||||
"Calmar (closed trades)",
|
||||
f"{strat_results['calmar']:.2f}" if "calmar" in strat_results else "N/A",
|
||||
),
|
||||
("SQN", f"{strat_results['sqn']:.2f}" if "sqn" in strat_results else "N/A"),
|
||||
(
|
||||
"Profit factor",
|
||||
@@ -367,12 +432,13 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
||||
"Avg. stake amount",
|
||||
fmt_coin(strat_results["avg_stake_amount"], stake),
|
||||
),
|
||||
("Market change", f"{strat_results['market_change']:.2%}"),
|
||||
(
|
||||
"Total trade volume",
|
||||
fmt_coin(strat_results["total_volume"], stake),
|
||||
),
|
||||
*short_metrics,
|
||||
("", ""), # Empty line to improve readability
|
||||
__EMPTY_LINE, # Empty line to improve readability
|
||||
(
|
||||
"Best Pair",
|
||||
f"{strat_results['best_pair']['key']} "
|
||||
@@ -428,10 +494,9 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
||||
f"{strat_results.get('timedout_exit_orders', 'N/A')}",
|
||||
),
|
||||
*entry_adjustment_metrics,
|
||||
("", ""), # Empty line to improve readability
|
||||
__EMPTY_LINE, # Empty line to improve readability
|
||||
*wallet_metrics,
|
||||
*drawdown_metrics,
|
||||
("Market change", f"{strat_results['market_change']:.2%}"),
|
||||
]
|
||||
print_rich_table(metrics, ["Metric", "Value"], summary="SUMMARY METRICS", justify="left")
|
||||
|
||||
|
||||
@@ -10,12 +10,16 @@ from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT
|
||||
from freqtrade.data.metrics import (
|
||||
calculate_cagr,
|
||||
calculate_calmar,
|
||||
calculate_calmar_from_balance,
|
||||
calculate_csum,
|
||||
calculate_expectancy,
|
||||
calculate_market_change,
|
||||
calculate_max_drawdown,
|
||||
calculate_max_drawdown_from_balance,
|
||||
calculate_sharpe,
|
||||
calculate_sharpe_from_balance,
|
||||
calculate_sortino,
|
||||
calculate_sortino_from_balance,
|
||||
calculate_sqn,
|
||||
)
|
||||
from freqtrade.ft_types import (
|
||||
@@ -59,15 +63,61 @@ def generate_wallet_stats(wallet_df: DataFrame, stake_currency: str) -> dict[str
|
||||
low_balance = total_quote.loc[low_idx]
|
||||
low_date = wallet.loc[low_idx, "date"]
|
||||
high_date = wallet.loc[high_idx, "date"]
|
||||
sharpe = calculate_sharpe_from_balance(wallet)
|
||||
sortino = calculate_sortino_from_balance(wallet)
|
||||
calmar = calculate_calmar_from_balance(wallet)
|
||||
try:
|
||||
drawdown = calculate_max_drawdown_from_balance(wallet)
|
||||
# max_relative_drawdown = Underwater
|
||||
drawdown_duration = drawdown.low_date - drawdown.high_date
|
||||
|
||||
except ValueError:
|
||||
drawdown = None
|
||||
drawdown_duration = timedelta()
|
||||
try:
|
||||
underwater = calculate_max_drawdown_from_balance(wallet, relative=True)
|
||||
except ValueError:
|
||||
underwater = None
|
||||
return {
|
||||
"start_balance": start_balance,
|
||||
"end_balance": end_balance,
|
||||
"high_balance": high_balance,
|
||||
"low_balance": low_balance,
|
||||
"sharpe": sharpe,
|
||||
"sortino": sortino,
|
||||
"calmar": calmar,
|
||||
"low_date": low_date.strftime(DATETIME_PRINT_FORMAT),
|
||||
"low_ts": int(low_date.timestamp() * 1000),
|
||||
"high_date": high_date.strftime(DATETIME_PRINT_FORMAT),
|
||||
"high_ts": int(high_date.timestamp() * 1000),
|
||||
# Drawdown metrics
|
||||
"max_drawdown_account": drawdown.relative_account_drawdown if drawdown else 0.0,
|
||||
"max_relative_drawdown": underwater.relative_account_drawdown if underwater else 0.0,
|
||||
"max_drawdown_abs": drawdown.drawdown_abs if drawdown else 0.0,
|
||||
"drawdown_start": (
|
||||
drawdown.high_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
if drawdown and drawdown.high_date is not None
|
||||
else None
|
||||
),
|
||||
"drawdown_start_ts": (
|
||||
int(drawdown.high_date.timestamp() * 1000)
|
||||
if drawdown and drawdown.high_date is not None
|
||||
else None
|
||||
),
|
||||
"drawdown_end": (
|
||||
drawdown.low_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
if drawdown and drawdown.low_date is not None
|
||||
else None
|
||||
),
|
||||
"drawdown_end_ts": (
|
||||
int(drawdown.low_date.timestamp() * 1000)
|
||||
if drawdown and drawdown.low_date is not None
|
||||
else None
|
||||
),
|
||||
"drawdown_duration": drawdown_duration,
|
||||
"drawdown_duration_s": drawdown_duration.total_seconds(),
|
||||
"max_drawdown_low": drawdown.low_value if drawdown else 0.0,
|
||||
"max_drawdown_high": drawdown.high_value if drawdown else 0.0,
|
||||
}
|
||||
|
||||
|
||||
@@ -290,7 +340,7 @@ def generate_strategy_comparison(bt_stats: dict) -> list[dict]:
|
||||
|
||||
def _get_resample_from_period(period: str) -> str:
|
||||
if period == "day":
|
||||
return "1d"
|
||||
return "1D"
|
||||
if period == "week":
|
||||
# Weekly defaulting to Monday.
|
||||
return "1W-MON"
|
||||
@@ -480,8 +530,8 @@ def generate_daily_stats(results: DataFrame) -> dict[str, Any]:
|
||||
"losing_days": 0,
|
||||
"daily_profit_list": [],
|
||||
}
|
||||
daily_profit_rel = results.resample("1d", on="close_date")["profit_ratio"].sum()
|
||||
daily_profit = results.resample("1d", on="close_date")["profit_abs"].sum().round(10)
|
||||
daily_profit_rel = results.resample("1D", on="close_date")["profit_ratio"].sum()
|
||||
daily_profit = results.resample("1D", on="close_date")["profit_abs"].sum().round(10)
|
||||
worst_rel = min(daily_profit_rel)
|
||||
best_rel = max(daily_profit_rel)
|
||||
worst = min(daily_profit)
|
||||
|
||||
@@ -263,7 +263,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
||||
trades["desc"] = trades.apply(
|
||||
lambda row: (
|
||||
f"{row['profit_ratio']:.2%}, "
|
||||
+ (f"{row['enter_tag']}, " if row["enter_tag"] is not None else "")
|
||||
+ (f"{row['enter_tag']}, " if pd.notna(row["enter_tag"]) else "")
|
||||
+ f"{row['exit_reason']}, "
|
||||
+ f"{row['trade_duration']} min"
|
||||
),
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any
|
||||
import psutil
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from dateutil.tz import tzlocal
|
||||
from numpy import inf, int64, isnan, mean, nan
|
||||
from numpy import inf, isnan, mean, nan
|
||||
from pandas import DataFrame, NaT, read_sql
|
||||
from sqlalchemy import func, select
|
||||
|
||||
@@ -794,7 +794,7 @@ class RPC:
|
||||
results = read_sql("wallet_history", con=Trade.session.bind, parse_dates=["timestamp"])
|
||||
|
||||
results = results.rename({"timestamp": "date"}, axis=1)
|
||||
results.loc[:, "__date_ts"] = results.loc[:, "date"].astype("int64") // 1000 // 1000
|
||||
results.loc[:, "__date_ts"] = results.loc[:, "date"].dt.as_unit("ms").astype("int64")
|
||||
# Exclude non-bot managed for now
|
||||
results_filtered = results.loc[results["bot_managed"]]
|
||||
|
||||
@@ -1536,7 +1536,9 @@ class RPC:
|
||||
df_cols = [col for col in dataframe_columns if col in cols_set]
|
||||
dataframe = dataframe.loc[:, df_cols]
|
||||
|
||||
dataframe.loc[:, "__date_ts"] = dataframe.loc[:, "date"].astype(int64) // 1000 // 1000
|
||||
dataframe.loc[:, "__date_ts"] = (
|
||||
dataframe.loc[:, "date"].dt.as_unit("ms").astype("int64")
|
||||
)
|
||||
# Move signal close to separate column when signal for easy plotting
|
||||
for sig_type in signals.keys():
|
||||
if sig_type in dataframe.columns:
|
||||
@@ -1546,7 +1548,7 @@ class RPC:
|
||||
|
||||
# band-aid until this is fixed:
|
||||
# https://github.com/pandas-dev/pandas/issues/45836
|
||||
datetime_types = ["datetime", "datetime64", "datetime64[ns, UTC]"]
|
||||
datetime_types = ["datetime", "datetime64", "datetimetz"]
|
||||
date_columns = dataframe.select_dtypes(include=datetime_types)
|
||||
for date_column in date_columns:
|
||||
# replace NaT with `None`
|
||||
|
||||
@@ -3,6 +3,7 @@ from freqtrade.util.datetime_helpers import (
|
||||
dt_from_ts,
|
||||
dt_humanize_delta,
|
||||
dt_now,
|
||||
dt_now_no_micro,
|
||||
dt_ts,
|
||||
dt_ts_def,
|
||||
dt_ts_none,
|
||||
@@ -39,6 +40,7 @@ __all__ = [
|
||||
"dt_from_ts",
|
||||
"dt_humanize_delta",
|
||||
"dt_now",
|
||||
"dt_now_no_micro",
|
||||
"dt_ts",
|
||||
"dt_ts_def",
|
||||
"dt_ts_none",
|
||||
|
||||
@@ -12,6 +12,13 @@ def dt_now() -> datetime:
|
||||
return datetime.now(UTC)
|
||||
|
||||
|
||||
def dt_now_no_micro() -> datetime:
|
||||
"""Return the current datetime in UTC without microseconds.
|
||||
Should not be used outside of tests.
|
||||
"""
|
||||
return dt_now().replace(microsecond=0)
|
||||
|
||||
|
||||
def dt_utc(
|
||||
year: int,
|
||||
month: int,
|
||||
|
||||
@@ -222,7 +222,6 @@ exclude-newer = "1 week"
|
||||
|
||||
[tool.uv.exclude-newer-package]
|
||||
ccxt = false
|
||||
cryptography = "1 days"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
-r requirements-freqai-rl.txt
|
||||
-r docs/requirements-docs.txt
|
||||
|
||||
ruff==0.15.8
|
||||
ruff==0.15.9
|
||||
mypy==1.20.0
|
||||
pre-commit==4.5.1
|
||||
pytest==9.0.2
|
||||
pytest==9.0.3
|
||||
pytest-asyncio==1.3.0
|
||||
pytest-cov==7.1.0
|
||||
pytest-mock==3.15.1
|
||||
|
||||
+4
-4
@@ -5,12 +5,12 @@ numexpr==2.14.1
|
||||
# Indicator libraries
|
||||
ft-pandas-ta==0.3.16
|
||||
ta-lib==0.6.8
|
||||
technical==1.5.4
|
||||
technical==1.6.0
|
||||
|
||||
ccxt==4.5.47
|
||||
ccxt==4.5.48
|
||||
cryptography==46.0.7
|
||||
aiohttp==3.13.5
|
||||
SQLAlchemy==2.0.48
|
||||
SQLAlchemy==2.0.49
|
||||
python-telegram-bot==22.7
|
||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||
httpx>=0.24.1
|
||||
@@ -39,7 +39,7 @@ sdnotify==0.3.2
|
||||
# API Server
|
||||
fastapi==0.135.3
|
||||
pydantic==2.12.5
|
||||
uvicorn==0.42.0
|
||||
uvicorn==0.43.0
|
||||
pyjwt==2.12.1
|
||||
aiofiles==25.1.0
|
||||
psutil==7.2.2
|
||||
|
||||
+1
-1
@@ -207,7 +207,7 @@ def generate_test_data(
|
||||
def generate_test_data_raw(timeframe: str, size: int, start: str = "2020-07-05", random_seed=42):
|
||||
"""Generates data in the ohlcv format used by ccxt"""
|
||||
df = generate_test_data(timeframe, size, start, random_seed)
|
||||
df["date"] = df.loc[:, "date"].astype(np.int64) // 1000 // 1000
|
||||
df["date"] = df.loc[:, "date"].dt.as_unit("ms").astype("int64")
|
||||
return list(list(x) for x in zip(*(df[x].values.tolist() for x in df.columns), strict=False))
|
||||
|
||||
|
||||
|
||||
+173
-1
@@ -1,7 +1,8 @@
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from pandas import DataFrame, DateOffset, Timestamp
|
||||
from pandas import DataFrame, DateOffset, Timestamp, to_datetime
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.btanalysis import (
|
||||
@@ -11,12 +12,16 @@ from freqtrade.data.history import load_data, load_pair_history
|
||||
from freqtrade.data.metrics import (
|
||||
calculate_cagr,
|
||||
calculate_calmar,
|
||||
calculate_calmar_from_balance,
|
||||
calculate_csum,
|
||||
calculate_expectancy,
|
||||
calculate_market_change,
|
||||
calculate_max_drawdown,
|
||||
calculate_max_drawdown_from_balance,
|
||||
calculate_sharpe,
|
||||
calculate_sharpe_from_balance,
|
||||
calculate_sortino,
|
||||
calculate_sortino_from_balance,
|
||||
calculate_sqn,
|
||||
calculate_underwater,
|
||||
combine_dataframes_with_mean,
|
||||
@@ -142,6 +147,48 @@ def test_calculate_max_drawdown(testdatadir):
|
||||
calculate_underwater(DataFrame())
|
||||
|
||||
|
||||
def test_calculate_max_drawdown_from_balance():
|
||||
balance_history = DataFrame(
|
||||
{
|
||||
"date": to_datetime(
|
||||
[
|
||||
"2025-01-01 00:00:00+00:00",
|
||||
"2025-01-01 12:00:00+00:00",
|
||||
"2025-01-01 18:00:00+00:00",
|
||||
"2025-01-04 00:00:00+00:00",
|
||||
],
|
||||
utc=True,
|
||||
),
|
||||
"total_quote": [100.0, 120.0, 80.0, 110.0],
|
||||
}
|
||||
)
|
||||
|
||||
drawdown = calculate_max_drawdown_from_balance(balance_history)
|
||||
assert isinstance(drawdown.relative_account_drawdown, float)
|
||||
assert pytest.approx(drawdown.relative_account_drawdown) == 1 / 3
|
||||
assert pytest.approx(drawdown.drawdown_abs) == 40
|
||||
assert pytest.approx(drawdown.current_high_value) == 20
|
||||
assert pytest.approx(drawdown.low_value) == -20
|
||||
assert pytest.approx(drawdown.high_value) == 20
|
||||
|
||||
assert drawdown.high_date == Timestamp("2025-01-01 12:00:00", tz="UTC")
|
||||
assert drawdown.low_date == Timestamp("2025-01-01 18:00:00", tz="UTC")
|
||||
|
||||
|
||||
def test_calculate_max_drawdown_from_balance_empty_or_short():
|
||||
with pytest.raises(ValueError, match=r"Balance-history dataframe empty\."):
|
||||
calculate_max_drawdown_from_balance(DataFrame())
|
||||
|
||||
one_point = DataFrame(
|
||||
{
|
||||
"date": to_datetime(["2025-01-01 00:00:00+00:00"], utc=True),
|
||||
"total_quote": [100.0],
|
||||
}
|
||||
)
|
||||
with pytest.raises(ValueError, match=r"Balance-history dataframe empty\."):
|
||||
calculate_max_drawdown_from_balance(one_point)
|
||||
|
||||
|
||||
def test_calculate_csum(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
@@ -200,6 +247,53 @@ def test_calculate_sortino(testdatadir):
|
||||
assert pytest.approx(sortino) == 35.17722
|
||||
|
||||
|
||||
def test_calculate_sortino_from_balance():
|
||||
balance_history = DataFrame(
|
||||
{
|
||||
"date": to_datetime(
|
||||
[
|
||||
"2025-01-01 00:00:00+00:00",
|
||||
"2025-01-02 00:00:00+00:00",
|
||||
"2025-01-03 00:00:00+00:00",
|
||||
"2025-01-04 00:00:00+00:00",
|
||||
"2025-01-05 00:00:00+00:00",
|
||||
],
|
||||
utc=True,
|
||||
),
|
||||
"total_quote": [100.0, 110.0, 104.5, 125.4, 112.86],
|
||||
}
|
||||
)
|
||||
|
||||
sortino = calculate_sortino_from_balance(balance_history)
|
||||
expected_returns = np.array([0.1, -0.05, 0.2, -0.1])
|
||||
expected_sortino = expected_returns.mean() / np.std(expected_returns[expected_returns < 0])
|
||||
expected_sortino *= np.sqrt(365)
|
||||
|
||||
assert isinstance(sortino, float)
|
||||
assert pytest.approx(sortino) == expected_sortino
|
||||
# Explicit assert
|
||||
assert pytest.approx(sortino) == 28.6574597
|
||||
|
||||
|
||||
def test_calculate_sortino_from_balance_empty_or_no_downside():
|
||||
assert calculate_sortino_from_balance(DataFrame()) == 0.0
|
||||
|
||||
positive_balance_history = DataFrame(
|
||||
{
|
||||
"date": to_datetime(
|
||||
[
|
||||
"2025-01-01 00:00:00+00:00",
|
||||
"2025-01-02 00:00:00+00:00",
|
||||
"2025-01-03 00:00:00+00:00",
|
||||
],
|
||||
utc=True,
|
||||
),
|
||||
"total_quote": [100.0, 110.0, 121.0],
|
||||
}
|
||||
)
|
||||
assert calculate_sortino_from_balance(positive_balance_history) == -100
|
||||
|
||||
|
||||
def test_calculate_sharpe(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
@@ -217,6 +311,45 @@ def test_calculate_sharpe(testdatadir):
|
||||
assert pytest.approx(sharpe) == 44.5078669
|
||||
|
||||
|
||||
def test_calculate_sharpe_from_balance():
|
||||
balance_history = DataFrame(
|
||||
{
|
||||
"date": to_datetime(
|
||||
[
|
||||
"2025-01-01 00:00:00+00:00",
|
||||
"2025-01-02 00:00:00+00:00",
|
||||
"2025-01-03 00:00:00+00:00",
|
||||
"2025-01-04 00:00:00+00:00",
|
||||
],
|
||||
utc=True,
|
||||
),
|
||||
"total_quote": [100.0, 110.0, 104.5, 125.4],
|
||||
}
|
||||
)
|
||||
|
||||
sharpe = calculate_sharpe_from_balance(balance_history)
|
||||
expected_returns = np.array([0.1, -0.05, 0.2])
|
||||
expected_sharpe = expected_returns.mean() / expected_returns.std() * np.sqrt(365)
|
||||
|
||||
assert isinstance(sharpe, float)
|
||||
assert pytest.approx(sharpe) == expected_sharpe
|
||||
|
||||
|
||||
def test_calculate_sharpe_from_balance_empty_or_flat():
|
||||
assert calculate_sharpe_from_balance(DataFrame()) == 0.0
|
||||
|
||||
flat_balance_history = DataFrame(
|
||||
{
|
||||
"date": to_datetime(
|
||||
["2025-01-01 00:00:00+00:00", "2025-01-02 00:00:00+00:00"],
|
||||
utc=True,
|
||||
),
|
||||
"total_quote": [100.0, 100.0],
|
||||
}
|
||||
)
|
||||
assert calculate_sharpe_from_balance(flat_balance_history) == -100
|
||||
|
||||
|
||||
def test_calculate_calmar(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
@@ -234,6 +367,45 @@ def test_calculate_calmar(testdatadir):
|
||||
assert pytest.approx(calmar) == 559.040508
|
||||
|
||||
|
||||
def test_calculate_calmar_from_balance():
|
||||
balance_history = DataFrame(
|
||||
{
|
||||
"date": to_datetime(
|
||||
[
|
||||
"2025-01-01 00:00:00+00:00",
|
||||
"2025-01-01 12:00:00+00:00",
|
||||
"2025-01-01 18:00:00+00:00",
|
||||
"2025-01-04 00:00:00+00:00",
|
||||
],
|
||||
utc=True,
|
||||
),
|
||||
"total_quote": [100.0, 120.0, 80.0, 110.0],
|
||||
}
|
||||
)
|
||||
|
||||
calmar = calculate_calmar_from_balance(balance_history)
|
||||
expected_returns_mean = ((110.0 - 100.0) / 100.0) / 3 * 100
|
||||
expected_calmar = expected_returns_mean / (1 / 3) * np.sqrt(365)
|
||||
|
||||
assert isinstance(calmar, float)
|
||||
assert pytest.approx(calmar) == expected_calmar
|
||||
|
||||
|
||||
def test_calculate_calmar_from_balance_empty_or_flat():
|
||||
assert calculate_calmar_from_balance(DataFrame()) == 0.0
|
||||
|
||||
flat_balance_history = DataFrame(
|
||||
{
|
||||
"date": to_datetime(
|
||||
["2025-01-01 00:00:00+00:00", "2025-01-02 00:00:00+00:00"],
|
||||
utc=True,
|
||||
),
|
||||
"total_quote": [100.0, 100.0],
|
||||
}
|
||||
)
|
||||
assert calculate_calmar_from_balance(flat_balance_history) == -100
|
||||
|
||||
|
||||
def test_calculate_sqn(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
|
||||
@@ -69,7 +69,7 @@ def make_response_from_url(start_date, end_date):
|
||||
"taker_buy_quote_volume,ignore"
|
||||
)
|
||||
df = pd.DataFrame(columns=cols.split(","), dtype=float)
|
||||
df["open_time"] = date_col.astype("int64") // 10**6
|
||||
df["open_time"] = date_col.as_unit("ms").astype("int64")
|
||||
df["open"] = df["high"] = df["low"] = df["close"] = df["volume"] = 1.0
|
||||
return df
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@ class TestCCXTExchange:
|
||||
# Check if last-timeframe is within the last 2 intervals
|
||||
now = datetime.now(UTC) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
|
||||
assert exch.klines(pair_tf).iloc[-1]["date"] >= timeframe_to_prev_date(timeframe, now)
|
||||
assert exch.klines(pair_tf)["date"].astype(int).iloc[0] // 1e6 == since_ms
|
||||
assert exch.klines(pair_tf)["date"].dt.as_unit("ms").astype("int64").iloc[0] == since_ms
|
||||
|
||||
def _ccxt__async_get_candle_history(
|
||||
self, exchange, pair: str, timeframe: str, candle_type: CandleType, factor: float = 0.9
|
||||
|
||||
@@ -28,6 +28,7 @@ from freqtrade.optimize.optimize_reports import (
|
||||
generate_trading_stats,
|
||||
show_sorted_pairlist,
|
||||
store_backtest_results,
|
||||
text_table_add_metrics,
|
||||
text_table_bt_results,
|
||||
text_table_strategy,
|
||||
)
|
||||
@@ -36,6 +37,7 @@ from freqtrade.optimize.optimize_reports.optimize_reports import (
|
||||
_get_resample_from_period,
|
||||
calc_streak,
|
||||
generate_tag_metrics,
|
||||
generate_wallet_stats,
|
||||
)
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
from freqtrade.util import dt_ts, format_duration
|
||||
@@ -616,6 +618,63 @@ def test_text_table_strategy(testdatadir, capsys):
|
||||
)
|
||||
|
||||
|
||||
def test_generate_wallet_stats_extended_metrics():
|
||||
wallet_df = pd.DataFrame(
|
||||
{
|
||||
"date": [
|
||||
dt_utc(2025, 1, 1, 0, 0, 0),
|
||||
dt_utc(2025, 1, 1, 12, 0, 0),
|
||||
dt_utc(2025, 1, 1, 18, 0, 0),
|
||||
dt_utc(2025, 1, 3, 0, 0, 0),
|
||||
],
|
||||
"currency": ["BTC", "BTC", "BTC", "BTC"],
|
||||
"rate": [1.0, 1.0, 1.0, 1.0],
|
||||
"balance": [100.0, 120.0, 80.0, 110.0],
|
||||
}
|
||||
)
|
||||
|
||||
stats = generate_wallet_stats(wallet_df, "BTC")
|
||||
|
||||
assert "sharpe" in stats
|
||||
assert "sortino" in stats
|
||||
assert "calmar" in stats
|
||||
assert "max_drawdown_account" in stats
|
||||
assert "max_drawdown_abs" in stats
|
||||
assert pytest.approx(stats["max_drawdown_account"]) == 1 / 3
|
||||
assert stats["drawdown_start"] == "2025-01-01 12:00:00"
|
||||
assert stats["drawdown_end"] == "2025-01-01 18:00:00"
|
||||
|
||||
|
||||
def test_text_table_add_metrics_shows_wallet_ratios(testdatadir, capsys):
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_stats(filename)
|
||||
strat_results = next(iter(bt_data["strategy"].values()))
|
||||
strat_results["wallet_stats"] = {
|
||||
"low_balance": 0.95,
|
||||
"high_balance": 1.12,
|
||||
"low_date": "2025-01-01 18:00:00",
|
||||
"high_date": "2025-01-01 12:00:00",
|
||||
"sharpe": 1.23,
|
||||
"sortino": 2.34,
|
||||
"calmar": 3.45,
|
||||
"max_drawdown_account": 0.12,
|
||||
"max_relative_drawdown": 0.15,
|
||||
"max_drawdown_abs": 0.05,
|
||||
"drawdown_start": "2025-01-01 12:00:00",
|
||||
"drawdown_end": "2025-01-01 18:00:00",
|
||||
"max_drawdown_high": 1.12,
|
||||
"max_drawdown_low": 0.95,
|
||||
}
|
||||
|
||||
text_table_add_metrics(strat_results)
|
||||
text = capsys.readouterr().out
|
||||
|
||||
assert "Sharpe (daily wallet balance)" in text
|
||||
assert "Sortino (daily wallet balance)" in text
|
||||
assert "Calmar (daily wallet balance)" in text
|
||||
assert "Max % of account underwater (balance)" in text
|
||||
|
||||
|
||||
def test_generate_periodic_breakdown_stats(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename).to_dict(orient="records")
|
||||
@@ -654,7 +713,7 @@ def test_generate_periodic_breakdown_stats(testdatadir):
|
||||
|
||||
|
||||
def test__get_resample_from_period():
|
||||
assert _get_resample_from_period("day") == "1d"
|
||||
assert _get_resample_from_period("day") == "1D"
|
||||
assert _get_resample_from_period("week") == "1W-MON"
|
||||
assert _get_resample_from_period("month") == "1ME"
|
||||
assert _get_resample_from_period("weekday") == "weekday"
|
||||
|
||||
@@ -22,6 +22,7 @@ from freqtrade.strategy.parameters import (
|
||||
)
|
||||
from freqtrade.strategy.strategy_validation import StrategyResultValidator
|
||||
from freqtrade.util import dt_now
|
||||
from freqtrade.util.datetime_helpers import dt_now_no_micro
|
||||
from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re
|
||||
|
||||
from .strats.strategy_test_v3 import StrategyTestV3
|
||||
@@ -33,7 +34,7 @@ _STRATEGY.dp = DataProvider({}, None, None)
|
||||
|
||||
|
||||
def test_returns_latest_signal(ohlcv_history):
|
||||
ohlcv_history.loc[1, "date"] = dt_now()
|
||||
ohlcv_history.loc[1, "date"] = dt_now_no_micro()
|
||||
# Take a copy to correctly modify the call
|
||||
mocked_history = ohlcv_history.copy()
|
||||
mocked_history["enter_long"] = 0
|
||||
@@ -160,7 +161,7 @@ def test_get_signal_exception_valueerror(mocker, caplog, ohlcv_history):
|
||||
def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
|
||||
# default_conf defines a 5m interval. we check interval * 2 + 5m
|
||||
# this is necessary as the last candle is removed (partial candles) by default
|
||||
ohlcv_history.loc[1, "date"] = dt_now() - timedelta(minutes=16)
|
||||
ohlcv_history.loc[1, "date"] = dt_now_no_micro() - timedelta(minutes=16)
|
||||
# Take a copy to correctly modify the call
|
||||
mocked_history = ohlcv_history.copy()
|
||||
mocked_history["exit_long"] = 0
|
||||
@@ -179,7 +180,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
|
||||
def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history):
|
||||
# default_conf defines a 5m interval. we check interval * 2 + 5m
|
||||
# this is necessary as the last candle is removed (partial candles) by default
|
||||
ohlcv_history.loc[1, "date"] = dt_now()
|
||||
ohlcv_history.loc[1, "date"] = dt_now_no_micro()
|
||||
# Take a copy to correctly modify the call
|
||||
mocked_history = ohlcv_history.copy()
|
||||
# Intentionally don't set sell column
|
||||
@@ -223,7 +224,7 @@ def test_ignore_expired_candle(default_conf):
|
||||
|
||||
|
||||
def test_assert_df_raise(mocker, caplog, ohlcv_history):
|
||||
ohlcv_history.loc[1, "date"] = dt_now() - timedelta(minutes=16)
|
||||
ohlcv_history.loc[1, "date"] = dt_now_no_micro() - timedelta(minutes=16)
|
||||
# Take a copy to correctly modify the call
|
||||
mocked_history = ohlcv_history.copy()
|
||||
mocked_history["sell"] = 0
|
||||
@@ -1040,6 +1041,7 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
|
||||
],
|
||||
)
|
||||
def test_pandas_warning_direct(ohlcv_history, function, raises, recwarn):
|
||||
recwarn.clear()
|
||||
df = _STRATEGY.populate_indicators(ohlcv_history, {"pair": "ETH/BTC"})
|
||||
if raises:
|
||||
assert len(recwarn) == 1
|
||||
@@ -1053,6 +1055,7 @@ def test_pandas_warning_direct(ohlcv_history, function, raises, recwarn):
|
||||
|
||||
|
||||
def test_pandas_warning_through_analyze_pair(ohlcv_history, mocker, recwarn):
|
||||
recwarn.clear()
|
||||
mocker.patch.object(_STRATEGY.dp, "ohlcv", return_value=ohlcv_history)
|
||||
_STRATEGY.analyze_pair("ETH/BTC")
|
||||
assert len(recwarn) == 0, f"warnings: {', '.join(str(w) for w in recwarn.list)}"
|
||||
|
||||
@@ -6,7 +6,9 @@ import time_machine
|
||||
from freqtrade.util import (
|
||||
dt_floor_day,
|
||||
dt_from_ts,
|
||||
dt_humanize_delta,
|
||||
dt_now,
|
||||
dt_now_no_micro,
|
||||
dt_ts,
|
||||
dt_ts_def,
|
||||
dt_ts_none,
|
||||
@@ -16,15 +18,17 @@ from freqtrade.util import (
|
||||
format_ms_time_det,
|
||||
shorten_date,
|
||||
)
|
||||
from freqtrade.util.datetime_helpers import dt_humanize_delta
|
||||
|
||||
|
||||
def test_dt_now():
|
||||
with time_machine.travel("2021-09-01 05:01:00 +00:00", tick=False) as t:
|
||||
with time_machine.travel("2021-09-01 05:01:00.123 +00:00", tick=False) as t:
|
||||
now = datetime.now(UTC)
|
||||
assert dt_now() == now
|
||||
assert dt_ts() == int(now.timestamp() * 1000)
|
||||
assert dt_ts(now) == int(now.timestamp() * 1000)
|
||||
assert dt_now().microsecond != 0.0
|
||||
assert dt_now_no_micro().microsecond == 0.0
|
||||
assert dt_now_no_micro() == now.replace(microsecond=0)
|
||||
|
||||
t.shift(timedelta(hours=5))
|
||||
assert dt_now() >= now
|
||||
|
||||
Reference in New Issue
Block a user