Compare commits
517 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e2313be7b | |||
| f535c4cff4 | |||
| 5f901d837c | |||
| f7b11c5166 | |||
| 2e92fccd7b | |||
| 09c60b9543 | |||
| 166476b21c | |||
| 3a775a5b65 | |||
| ab9b6989be | |||
| 1f69cafd16 | |||
| 7030b0bfcc | |||
| f0dd3e4ab8 | |||
| 402ef216c3 | |||
| 5f585e330f | |||
| 6d04874e26 | |||
| 4b45e3f3d2 | |||
| 4359de7cc5 | |||
| 48015a9b1b | |||
| 34df7b1793 | |||
| 429276cac6 | |||
| e7154c93a9 | |||
| 523a9d644b | |||
| 3a6311b7fc | |||
| f6dbca35f3 | |||
| 07e1088839 | |||
| 74d373e37d | |||
| 6ce4d1a7c5 | |||
| 402a3bb2c2 | |||
| 961eaef2a5 | |||
| 6e49a90730 | |||
| 246a9049e6 | |||
| 816f81e5b4 | |||
| 49e32b2600 | |||
| b6993f0b24 | |||
| 58079a8b50 | |||
| bdf080155d | |||
| 52fede6307 | |||
| 7d6c80fa7c | |||
| 814111adb9 | |||
| f0f4191dcf | |||
| 9a80a11577 | |||
| 5551a287a8 | |||
| b0f7c91548 | |||
| 9232e17af4 | |||
| d962137d24 | |||
| 22c91137bb | |||
| 92b8bdd113 | |||
| 3c5178b002 | |||
| 8231005225 | |||
| 2646313c11 | |||
| 12b4875e24 | |||
| 6a98df1c71 | |||
| a5d24f14cb | |||
| 50f914d6d5 | |||
| b4422131fb | |||
| f61b50dbfc | |||
| ce590cced7 | |||
| a417af1ff0 | |||
| d2e650d188 | |||
| 088516b2ed | |||
| 797ea46bb3 | |||
| 96c0a6296f | |||
| e524e030ef | |||
| e19d96f85c | |||
| 0672d8f094 | |||
| 780858f538 | |||
| 7ec495be67 | |||
| d27aef2063 | |||
| d7d07bde75 | |||
| f54ccde5fc | |||
| e1d7c7a081 | |||
| d593384219 | |||
| 5c737d977d | |||
| 22c0af9bb1 | |||
| 1c21b092e6 | |||
| 2325e5f535 | |||
| dbaece9462 | |||
| edd33ab77e | |||
| 38934ddda2 | |||
| 5c9455c32a | |||
| 9e2eca3b8b | |||
| 82a487ec50 | |||
| 466db15ccb | |||
| c7dfb005ba | |||
| 82ae8b64e4 | |||
| 1ffc2e47a1 | |||
| a5c392e176 | |||
| 8f9754dd54 | |||
| b3b3b1d134 | |||
| 26fd70ce4b | |||
| 18aed385f9 | |||
| e6db0ab00d | |||
| 8ed1e6c394 | |||
| da62e614d7 | |||
| ca94909ccc | |||
| 131c1aaf13 | |||
| 5f06b1d43e | |||
| c999de6291 | |||
| a42c8faeca | |||
| ecfbfa2a35 | |||
| 6b86214ca8 | |||
| df6f53ba30 | |||
| 0888f6dba0 | |||
| 5d3c798dfd | |||
| 93d2397f72 | |||
| 05e759a38c | |||
| 67eaa4ac49 | |||
| e8aeebb123 | |||
| d121e97067 | |||
| bdf5c4de8e | |||
| 7492cf8bf5 | |||
| d29d4cba66 | |||
| 655d61e560 | |||
| ba6d210662 | |||
| aff46acdd0 | |||
| 5a16cbe602 | |||
| 519f2d93ba | |||
| 646c091e02 | |||
| 12c3698480 | |||
| c86acd20d6 | |||
| 60b787edc9 | |||
| eec33190d2 | |||
| 6e38759bd1 | |||
| 6a242356a0 | |||
| 04d2ea8773 | |||
| d78ffd7a05 | |||
| 3f21a18f46 | |||
| c4514f94cf | |||
| af39ec1e07 | |||
| c02530759d | |||
| 6df42502ac | |||
| fea13d1d3d | |||
| c7685febcf | |||
| abf5f2b49a | |||
| 24319585b4 | |||
| 812f357d7e | |||
| 51431d9533 | |||
| eb81c70523 | |||
| b52805cd96 | |||
| cb1266225e | |||
| 33ba2ad17c | |||
| 2f2b36f648 | |||
| aa1de750e9 | |||
| 68410f2f37 | |||
| 1f00c9d680 | |||
| a91fd66abc | |||
| 4036375759 | |||
| 9eb668d08a | |||
| ed22b4e09b | |||
| 52090d7b8e | |||
| 3a065c27e8 | |||
| a14f54735c | |||
| c5697c7d68 | |||
| 6381a32685 | |||
| 42230fdd0e | |||
| 4934a83a2d | |||
| 0ef0ff2a78 | |||
| 80d23f1ba1 | |||
| 59b05ece77 | |||
| e467405e60 | |||
| 1f92c3fd70 | |||
| 74e8875280 | |||
| 3c44a013e5 | |||
| 8546542a16 | |||
| f56460cc1f | |||
| 54f25b5b8f | |||
| 541b976b8e | |||
| b6c7e24cad | |||
| 22d624a25d | |||
| d482117fd1 | |||
| 835f00b106 | |||
| 55c3b83fa8 | |||
| 348afa46f0 | |||
| a7890a964f | |||
| ba57ab9a1b | |||
| ac6b396ed3 | |||
| 570bfef4b3 | |||
| 100babc056 | |||
| 5ab8338c3e | |||
| 600b6a7d23 | |||
| f6e465a0ba | |||
| bea006880b | |||
| a7e35f5d78 | |||
| 5fb963abbf | |||
| 351fe62d18 | |||
| 65a713a569 | |||
| 646531cbfc | |||
| 8233ca67bb | |||
| d991069efc | |||
| d86bca9f00 | |||
| 82f71d8e4b | |||
| 315a1baf2c | |||
| e5bd33061b | |||
| 06ed6e6b23 | |||
| 4a578148e2 | |||
| 2c0a029187 | |||
| a3b83afa84 | |||
| 6c2a5b460e | |||
| aa302fe531 | |||
| f2c2e84a74 | |||
| d73a173c6e | |||
| 110b50a3cb | |||
| 36f5afe910 | |||
| 15f79c19a0 | |||
| 7135466092 | |||
| e4c25ed07e | |||
| 273ac9a9c0 | |||
| 7007e71dcf | |||
| 14247854ce | |||
| d863adc8af | |||
| 2e33b1dc56 | |||
| d56ae991c2 | |||
| 2913af90ca | |||
| 553e45c948 | |||
| 2364a08fd6 | |||
| eed5de4045 | |||
| 71a4f88c8b | |||
| db1162e0a1 | |||
| 40a791985e | |||
| 157669b775 | |||
| 3d8d101bfc | |||
| 7e3c8fc8ab | |||
| c58ac19adf | |||
| 35d038cbf7 | |||
| 13a5c54338 | |||
| c0a439fb5f | |||
| 5ce2203786 | |||
| f3931c8587 | |||
| 828abf6326 | |||
| 33ebbab6a8 | |||
| 0a49816b24 | |||
| c77ac9f101 | |||
| 820a13f37a | |||
| 2a29fccd1d | |||
| b857e19392 | |||
| b4bdc66c39 | |||
| 98b56a49c8 | |||
| 791fbe8054 | |||
| cedfb2a51f | |||
| 93b6c52aad | |||
| 0ef85e161e | |||
| 26aed91bbf | |||
| 3c2ddda203 | |||
| c86484b152 | |||
| beec9123d4 | |||
| a33eb51f36 | |||
| 373cd8141c | |||
| 360f294f45 | |||
| a64ab2629b | |||
| 5d88923e03 | |||
| 84cbb1eb01 | |||
| 1c764d622d | |||
| fb6627335b | |||
| 436d3b1bbc | |||
| 308c80c7b2 | |||
| a4a69bd16d | |||
| 9bd68dd63a | |||
| 670cfa2087 | |||
| 7a69cfa62b | |||
| 142dd3fd25 | |||
| d102d3cac8 | |||
| 2b7bcdb594 | |||
| 9ccbf15080 | |||
| 16717f06e9 | |||
| 7d77e16271 | |||
| f501619434 | |||
| 7c9a618c72 | |||
| 16374ef7f1 | |||
| bb4d2f8f1a | |||
| 52fc180c75 | |||
| 87b313cd53 | |||
| 74c88f77c0 | |||
| 6cb459f37b | |||
| 00848c9a96 | |||
| e8adf01b18 | |||
| d28fce7ed8 | |||
| 390d504cf2 | |||
| 1799aa33f9 | |||
| dcff1c4317 | |||
| 121bdab8fe | |||
| 9a48feea35 | |||
| 0955ccadfe | |||
| c778e44e80 | |||
| 18933a6e20 | |||
| 182bdaceb5 | |||
| 6ae4394628 | |||
| 1bdb1539b1 | |||
| 2765277cd0 | |||
| 085a9be186 | |||
| e279c0333f | |||
| 5e7246d23b | |||
| fe86163730 | |||
| b5d467c943 | |||
| c9fd8a619a | |||
| 690080e180 | |||
| 583431e9d5 | |||
| 4b82f82d9c | |||
| 63d6001415 | |||
| ad6cf90037 | |||
| 558c65a85c | |||
| 8cb72e1fa3 | |||
| 4870d45b04 | |||
| 4436acea68 | |||
| e8eb6d7018 | |||
| f1a6e61dbd | |||
| 4abb871c16 | |||
| a6eccaec69 | |||
| 1747724206 | |||
| 616900330b | |||
| bbe5e22730 | |||
| 8b396ae3a2 | |||
| 7f511cbe82 | |||
| 12f5b9bc5b | |||
| d152e3e47d | |||
| 39908c60e2 | |||
| 74a1e48a1c | |||
| b507aebf1c | |||
| ad293c28d0 | |||
| e4c7b5511a | |||
| 85fd070899 | |||
| e658490593 | |||
| 621e78e7ce | |||
| fc5801a0b7 | |||
| 22cd1357f8 | |||
| ead68e0a51 | |||
| 80b87d2592 | |||
| 0ac43042cc | |||
| 590a69ca42 | |||
| e44a36cedd | |||
| 131ab77085 | |||
| ba07de8d00 | |||
| 6ee34aa482 | |||
| d5686d548c | |||
| 4917a0abe5 | |||
| 93f2cf5db7 | |||
| 38a470098f | |||
| b0eb5c77ec | |||
| 0d5bd94f43 | |||
| 735284dccd | |||
| deb0dd80c8 | |||
| 12f815e157 | |||
| 059636f1ea | |||
| 793ca9c3da | |||
| 323a28c0bc | |||
| 77567f6315 | |||
| dda448c10d | |||
| b7df0de33c | |||
| a89ef23407 | |||
| b90538f47d | |||
| ee6b7f2f95 | |||
| b4aa6231e0 | |||
| 6bd57cc3b5 | |||
| 26db014782 | |||
| 8e3ca302b0 | |||
| 14aeb56ff9 | |||
| 9cbb8ec1c2 | |||
| faa5c4be39 | |||
| 45b6ec95a4 | |||
| 91b921cc88 | |||
| 123897ea89 | |||
| 31cf2dd46d | |||
| 06a53924d7 | |||
| 993771c5d3 | |||
| f08f19d1cb | |||
| 515912f14d | |||
| df8465a9f3 | |||
| 1333cb3181 | |||
| 18fa981690 | |||
| 60425e6237 | |||
| 3b5f1d52cd | |||
| 519721b58b | |||
| 3ae7823983 | |||
| 7182e6960e | |||
| 8596042065 | |||
| c2e92250b5 | |||
| d2ed5c6736 | |||
| 8e023a7bf3 | |||
| b3711fdf6c | |||
| 681876e0e5 | |||
| c3502991d3 | |||
| 6cc22fcf74 | |||
| 2f011e5421 | |||
| 4322acbbb8 | |||
| 50cba51008 | |||
| 7498f20baa | |||
| a1b3e59491 | |||
| 4fcbce2ce5 | |||
| 22699ee6ed | |||
| aec5450036 | |||
| 2c5a150a7b | |||
| f845dc71b1 | |||
| 61274252af | |||
| 00d903cb3f | |||
| 29f235441b | |||
| 872b59c9cd | |||
| be7f5f4e26 | |||
| 82c1f096de | |||
| ed7887c426 | |||
| e58f7b0344 | |||
| 32eaae5dcf | |||
| 38c74e5d1a | |||
| e89a31c6d1 | |||
| 3eb8de633c | |||
| 0ee3e4304f | |||
| 228dfe8ab2 | |||
| bfcc2aa153 | |||
| 39e2ecb6a2 | |||
| 0fcfd933ea | |||
| b9a4918d61 | |||
| 0d7127bb6a | |||
| cfd048e74e | |||
| c911332da3 | |||
| 43a849d45d | |||
| 3ad502b073 | |||
| 6d4ad393d5 | |||
| 69cc238700 | |||
| 9a9b4e15f3 | |||
| fcab9460fb | |||
| 58130572a9 | |||
| f82d2fe796 | |||
| 577464c12d | |||
| 00bd3e5a5c | |||
| bc2be74b36 | |||
| 3543437fc5 | |||
| 4b278941b8 | |||
| 8a33b71351 | |||
| a09ca38de2 | |||
| 4d4fee9874 | |||
| f2dfe1788a | |||
| 0e82c0458a | |||
| 2ec2626277 | |||
| 04f4f327df | |||
| 34ad58e2da | |||
| f78aa7b39a | |||
| a174461952 | |||
| 6c164683b7 | |||
| 9cdd8659ec | |||
| 795df94817 | |||
| 40f87c0d30 | |||
| 1380940565 | |||
| 595309c9ed | |||
| 78dde7d1bc | |||
| 2a0da08413 | |||
| fd7aff6063 | |||
| c41584a548 | |||
| 1e6d832f71 | |||
| 6f2ba13b0c | |||
| 78141c7216 | |||
| 8e91fea11f | |||
| 0f8c6d456e | |||
| 2a1b3ae7ba | |||
| 0a94a6d332 | |||
| 75a3651493 | |||
| 0dad2b36ab | |||
| 70834b6491 | |||
| 31269c7ce0 | |||
| f0a5bbe511 | |||
| 863124d67e | |||
| 6d1e4d8f45 | |||
| 5f12c81768 | |||
| 31341afcef | |||
| 383b305b74 | |||
| 84244e7ca7 | |||
| cc06e9baec | |||
| d5ac8a5b0b | |||
| 00a4a743d2 | |||
| 650cb1ef8c | |||
| 76ea4ea14f | |||
| 90502a6ac0 | |||
| f9fe909847 | |||
| 7085103e43 | |||
| c5ee96b66a | |||
| d454a10dea | |||
| e3d03f586d | |||
| e8d748535f | |||
| 1cdb386374 | |||
| 8205cb25b8 | |||
| 511b817e84 | |||
| de94170424 | |||
| 6e9b60c0b0 | |||
| b8cf38cb6f | |||
| b394705545 | |||
| 9b99c5b3f3 | |||
| e77e901df1 | |||
| b26c37af62 | |||
| 414fe81b27 | |||
| 00de8077f7 | |||
| 1dde1ad0ed | |||
| 2708343a2d | |||
| 61eb3306dd | |||
| e71329c82b | |||
| dc4adccaaf | |||
| 45b9c8c53f | |||
| bcd033bd0d | |||
| ce8732b0cd | |||
| bcf6c244b0 | |||
| 9023ca4f56 | |||
| 29b7cbbb12 | |||
| b2d5343a4e | |||
| 301aff141a | |||
| 245ca4bc2a | |||
| f32d25daac | |||
| 789f4aa4f8 | |||
| 2045fa6427 | |||
| 922d5893f8 | |||
| 3ebc7bfcfc | |||
| c892b07eef | |||
| 7a23da4deb | |||
| 670999abda | |||
| e2b5e209f6 | |||
| 645f70e216 | |||
| b59db3c690 | |||
| 6cef73947c | |||
| 33dbe32e5c | |||
| 8caf7a888e | |||
| 033050e6cf | |||
| ea6d23bd2d |
@@ -46,8 +46,9 @@ runs:
|
||||
id: tags
|
||||
env:
|
||||
BRANCH_NAME_INPUT: ${{ github.event.inputs.branch_name }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||
BRANCH_NAME="${BRANCH_NAME_INPUT}"
|
||||
else
|
||||
BRANCH_NAME="${GITHUB_REF##*/}"
|
||||
|
||||
@@ -2,29 +2,41 @@ name: Binance Leverage tiers update
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 3 * * 4"
|
||||
- cron: "25 3 * * 4"
|
||||
# on demand
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
auto-update:
|
||||
name: "Auto Update Binance Leverage Tiers"
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: develop
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: false
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Install ccxt
|
||||
run: pip install ccxt
|
||||
run: uv pip install ccxt orjson
|
||||
|
||||
- name: Run leverage tier update
|
||||
env:
|
||||
@@ -34,7 +46,7 @@ jobs:
|
||||
run: python build_helpers/binance_update_lev_tiers.py
|
||||
|
||||
|
||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||
add-paths: freqtrade/exchange/binance_leverage_tiers.json
|
||||
|
||||
+28
-29
@@ -16,8 +16,8 @@ on:
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}"
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
repository-projects: read
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: "Tests and Linting"
|
||||
@@ -28,17 +28,17 @@ jobs:
|
||||
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
$PSVersionTable
|
||||
Get-PSRepository | Format-List *
|
||||
Set-PSRepository psgallery -InstallationPolicy trusted
|
||||
Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force -SkipPublisherCheck
|
||||
Install-Module -Name Pester -RequiredVersion 5.7.1 -Confirm:$false -Force -SkipPublisherCheck
|
||||
$Error.clear()
|
||||
Invoke-Pester -Path "tests" -CI
|
||||
if ($Error.Length -gt 0) {exit 1}
|
||||
@@ -178,12 +178,12 @@ jobs:
|
||||
name: "Mypy Version Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -196,11 +196,11 @@ jobs:
|
||||
name: "Pre-commit checks"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
name: "Documentation build"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -218,7 +218,7 @@ jobs:
|
||||
./tests/test_docs.sh
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -241,17 +241,17 @@ jobs:
|
||||
name: "Tests and Linting - Online tests"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -275,6 +275,7 @@ jobs:
|
||||
|
||||
# Notify only once - when CI completes (and after deploy) in case it's successful
|
||||
notify-complete:
|
||||
name: "Notify CI Completion"
|
||||
needs: [
|
||||
build,
|
||||
build-linux-online
|
||||
@@ -282,8 +283,6 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
# Discord notification can't handle schedule events
|
||||
if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade'
|
||||
permissions:
|
||||
repository-projects: read
|
||||
steps:
|
||||
|
||||
- name: Check user permission
|
||||
@@ -321,12 +320,12 @@ jobs:
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -336,7 +335,7 @@ jobs:
|
||||
python -m build --sdist --wheel
|
||||
|
||||
- name: Upload artifacts 📦
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.1.0
|
||||
with:
|
||||
name: freqtrade-build
|
||||
path: |
|
||||
@@ -349,7 +348,7 @@ jobs:
|
||||
python -m build --sdist --wheel ft_client
|
||||
|
||||
- name: Upload artifacts 📦
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.1.0
|
||||
with:
|
||||
name: freqtrade-client-build
|
||||
path: |
|
||||
@@ -365,15 +364,15 @@ jobs:
|
||||
name: testpypi
|
||||
url: https://test.pypi.org/p/freqtrade
|
||||
permissions:
|
||||
id-token: write
|
||||
id-token: write # Needed for pypa/gh-action-pypi-publish
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download artifact 📦
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
pattern: freqtrade*-build
|
||||
path: dist
|
||||
@@ -394,15 +393,15 @@ jobs:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/freqtrade
|
||||
permissions:
|
||||
id-token: write
|
||||
id-token: write # Needed for pypa/gh-action-pypi-publish
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download artifact 📦
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
pattern: freqtrade*-build
|
||||
path: dist
|
||||
@@ -420,7 +419,7 @@ jobs:
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||
uses: ./.github/workflows/docker-build.yml
|
||||
permissions:
|
||||
packages: write
|
||||
packages: write # Needed to push package versions
|
||||
contents: read
|
||||
secrets:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -434,6 +433,6 @@ jobs:
|
||||
# Only run on push, schedule, or release events
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule') && github.repository == 'freqtrade/freqtrade'
|
||||
permissions:
|
||||
packages: write
|
||||
packages: write # Needed to delete package versions
|
||||
with:
|
||||
package_name: 'freqtrade'
|
||||
|
||||
@@ -11,6 +11,9 @@ on:
|
||||
# disable permissions for all of the available permissions
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-docs:
|
||||
@@ -19,12 +22,12 @@ jobs:
|
||||
name: Deploy Docs through mike
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
@@ -17,18 +17,21 @@ concurrency:
|
||||
group: "${{ github.workflow }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: "Build and Push Devcontainer Image"
|
||||
permissions:
|
||||
packages: write
|
||||
packages: write # Needed to push package versions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -40,3 +43,14 @@ jobs:
|
||||
imageName: ghcr.io/${{ github.repository }}-devcontainer
|
||||
cacheFrom: ghcr.io/${{ github.repository }}-devcontainer
|
||||
push: always
|
||||
|
||||
|
||||
packages-cleanup:
|
||||
name: "Docker Package Cleanup"
|
||||
uses: ./.github/workflows/packages-cleanup.yml
|
||||
# Only run on push, schedule, or release events
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule') && github.repository == 'freqtrade/freqtrade'
|
||||
permissions:
|
||||
packages: write # Needed to delete package versions
|
||||
with:
|
||||
package_name: 'freqtrade-devcontainer'
|
||||
|
||||
@@ -17,6 +17,10 @@ on:
|
||||
default: 'develop'
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -33,7 +37,7 @@ jobs:
|
||||
if: github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -53,7 +57,7 @@ jobs:
|
||||
uses: ./.github/actions/docker-tags
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -159,14 +163,14 @@ jobs:
|
||||
deploy-arm:
|
||||
name: "Deploy Docker ARM64"
|
||||
permissions:
|
||||
packages: write
|
||||
packages: write # Needed to push package versions
|
||||
needs: [ deploy-docker ]
|
||||
# Only run on 64bit machines
|
||||
runs-on: [self-hosted, linux, ARM64]
|
||||
if: github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -175,13 +179,13 @@ jobs:
|
||||
uses: ./.github/actions/docker-tags
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to github
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -4,14 +4,19 @@ on:
|
||||
branches:
|
||||
- stable
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# disable permissions for all of the available permissions
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
dockerHubDescription:
|
||||
name: "Update Docker Hub Description"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -25,20 +25,26 @@ on:
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
PACKAGE_NAME: "freqtrade"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
deploy-docker:
|
||||
name: "Delete Packages"
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.repository == 'freqtrade/freqtrade'
|
||||
permissions:
|
||||
packages: write
|
||||
packages: write # Needed to delete package versions
|
||||
|
||||
steps:
|
||||
- name: "Delete untagged Package Versions"
|
||||
uses: actions/delete-package-versions@v5
|
||||
uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0
|
||||
with:
|
||||
package-name: ${{ inputs.package_name || env.PACKAGE_NAME }}
|
||||
package-type: 'container'
|
||||
|
||||
@@ -9,15 +9,20 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
auto-update:
|
||||
name: Auto-update pre-commit hooks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -28,7 +33,7 @@ jobs:
|
||||
- name: Run auto-update
|
||||
run: pre-commit autoupdate
|
||||
|
||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||
add-paths: .pre-commit-config.yaml
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
name: GitHub Actions Security Analysis with zizmor 🌈
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- stable
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- stable
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Run zizmor 🌈
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
# contents: read # only needed for private repos
|
||||
# actions: read # only needed for private repos
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0
|
||||
@@ -0,0 +1,34 @@
|
||||
name: GitHub Actions Security Analysis with zizmor 🌈
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- stable
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- stable
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Run zizmor 🌈
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||
# contents: read # Only needed for private repos. Needed to clone the repo.
|
||||
# actions: read # Only needed for private repos. Needed for upload-sarif to read workflow run info.
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
|
||||
@@ -28,15 +28,15 @@ repos:
|
||||
additional_dependencies:
|
||||
- types-cachetools==6.2.0.20251022
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.32.4.20250913
|
||||
- types-requests==2.32.4.20260107
|
||||
- types-tabulate==0.9.0.20241207
|
||||
- types-python-dateutil==2.9.0.20251115
|
||||
- scipy-stubs==1.16.3.3
|
||||
- SQLAlchemy==2.0.45
|
||||
- types-python-dateutil==2.9.0.20260124
|
||||
- scipy-stubs==1.17.0.2
|
||||
- SQLAlchemy==2.0.46
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: "7.0.0"
|
||||
rev: "8.0.0"
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
@@ -44,7 +44,7 @@ repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.14.10'
|
||||
rev: 'v0.15.2'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
@@ -83,6 +83,6 @@ repos:
|
||||
|
||||
# Ensure github actions remain safe
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.19.0
|
||||
rev: v1.22.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
+3
-3
@@ -12,7 +12,8 @@ Few pointers for contributions:
|
||||
- Stick to english in both commit messages, PR descriptions and code comments and variable names.
|
||||
- New features need to contain unit tests, must pass CI (run pre-commit and pytest to get an early feedback) and should be documented with the introduction PR.
|
||||
- PR's can be declared as draft - signaling Work in Progress for Pull Requests (which are not finished). We'll still aim to provide feedback on draft PR's in a timely manner.
|
||||
- If you're using AI for your PR, please both mention it in the PR description and do a thorough review of the generated code. The final responsibility for the code with the PR author, not with the AI.
|
||||
- If you're using AI for your PR, please both mention it in the PR description and do a thorough review of the generated code yourself.
|
||||
The final responsibility for the code with the PR author, not with the AI, which also means that commits must be linked to your (human) account, not some generic AI account.
|
||||
|
||||
If you are unsure, discuss the feature on our [discord server](https://discord.gg/p7nuUNVfP7) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a Pull Request.
|
||||
|
||||
@@ -24,8 +25,7 @@ Best start by reading the [documentation](https://www.freqtrade.io/) to get a fe
|
||||
|
||||
### 1. Run unit tests
|
||||
|
||||
All unit tests must pass. If a unit test is broken, change your code to
|
||||
make it pass. It means you have introduced a regression.
|
||||
All unit tests must pass. If a unit test is broken, change your code to make it pass. It means you have introduced a regression.
|
||||
|
||||
#### Test the whole project
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:3.13.11-slim-bookworm AS base
|
||||
FROM python:3.13.12-slim-trixie AS base
|
||||
|
||||
# Setup env
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
[](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
|
||||
[](https://doi.org/10.21105/joss.04864)
|
||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||
[](https://codecov.io/gh/freqtrade/freqtrade)
|
||||
[](https://www.freqtrade.io)
|
||||
[](https://discord.gg/p7nuUNVfP7)
|
||||
|
||||
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
||||
|
||||
@@ -24,7 +25,9 @@ hesitate to read the source code and understand the mechanism of this bot.
|
||||
|
||||
## Supported Exchange marketplaces
|
||||
|
||||
Please read the [exchange-specific notes](docs/exchanges.md) to learn about special configurations that maybe needed for each exchange.
|
||||
Please read the [exchange-specific notes](https://www.freqtrade.io/en/stable/exchanges/) to learn about special configurations that maybe needed for each exchange.
|
||||
|
||||
### Supported Spot Exchanges
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [BingX](https://bingx.com/invite/0EM9RX)
|
||||
@@ -39,7 +42,7 @@ Please read the [exchange-specific notes](docs/exchanges.md) to learn about spec
|
||||
- [X] [MyOKX](https://okx.com/) (OKX EEA)
|
||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||
|
||||
### Supported Futures Exchanges (experimental)
|
||||
### Supported Futures Exchanges
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Bitget](https://www.bitget.com/)
|
||||
@@ -48,7 +51,7 @@ Please read the [exchange-specific notes](docs/exchanges.md) to learn about spec
|
||||
- [X] [OKX](https://okx.com/)
|
||||
- [X] [Bybit](https://bybit.com/)
|
||||
|
||||
Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in.
|
||||
Please make sure to read the [exchange specific notes](https://www.freqtrade.io/en/stable/exchanges/), as well as the [trading with leverage](https://www.freqtrade.io/en/stable/leverage/) documentation before diving in.
|
||||
|
||||
### Community tested
|
||||
|
||||
@@ -140,7 +143,7 @@ options:
|
||||
|
||||
### Telegram RPC commands
|
||||
|
||||
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on the [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
|
||||
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on the [documentation](https://www.freqtrade.io/en/stable/telegram-usage/)
|
||||
|
||||
- `/start`: Starts the trader.
|
||||
- `/stop`: Stops the trader.
|
||||
|
||||
BIN
Binary file not shown.
@@ -1057,7 +1057,8 @@
|
||||
},
|
||||
"jwt_secret_key": {
|
||||
"description": "Secret key for JWT authentication.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "somethingRandomSomethingRandom123"
|
||||
},
|
||||
"CORS_origins": {
|
||||
"description": "List of allowed CORS origins.",
|
||||
@@ -1080,7 +1081,8 @@
|
||||
"listen_ip_address",
|
||||
"listen_port",
|
||||
"username",
|
||||
"password"
|
||||
"password",
|
||||
"jwt_secret_key"
|
||||
]
|
||||
},
|
||||
"db_url": {
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8080,
|
||||
"verbosity": "error",
|
||||
"jwt_secret_key": "somethingrandom",
|
||||
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "SuperSecurePassword"
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
"listen_port": 8080,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "somethingrandom",
|
||||
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "SuperSecurePassword",
|
||||
@@ -215,4 +215,4 @@
|
||||
"reduce_df_footprint": false,
|
||||
"dataformat_ohlcv": "feather",
|
||||
"dataformat_trades": "feather"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8080,
|
||||
"verbosity": "error",
|
||||
"jwt_secret_key": "somethingrandom",
|
||||
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "SuperSecurePassword"
|
||||
|
||||
@@ -41,7 +41,7 @@ ranging from the simplest (0) to the most detailed per pair, per buy and per sel
|
||||
* 1: profit summaries grouped by enter_tag
|
||||
* 2: profit summaries grouped by enter_tag and exit_tag
|
||||
* 3: profit summaries grouped by pair and enter_tag
|
||||
* 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
||||
* 4: profit summaries grouped by pair, enter_tag and exit_tag (this can get quite large)
|
||||
* 5: profit summaries grouped by exit_tag
|
||||
|
||||
More options are available by running with the `-h` option.
|
||||
@@ -52,11 +52,10 @@ By default, `backtesting-analysis` processes the most recent backtest results in
|
||||
If you want to analyze results from an earlier backtest, use the `--backtest-filename` option to specify the desired file. This lets you revisit and re-analyze historical backtest outputs at any time by providing the filename of the relevant backtest result:
|
||||
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange <timerange> --export signals --backtest-filename backtest-result-2025-03-05_20-38-34.zip
|
||||
freqtrade backtesting -c <config.json> --strategy <strategy_name> --timerange <timerange> --export signals --backtest-filename backtest-result-2025-03-05_20-38-34.zip
|
||||
```
|
||||
|
||||
You should see some output similar to below in the logs with the name of the timestamped
|
||||
filename that was exported:
|
||||
You should see some output similar to below in the logs with the name of the timestamped filename that was exported:
|
||||
|
||||
```
|
||||
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "mystrat_backtest-2022-06-14_16-28-32.json"
|
||||
@@ -64,14 +63,14 @@ filename that was exported:
|
||||
|
||||
You can then use that filename in `backtesting-analysis`:
|
||||
|
||||
```
|
||||
freqtrade backtesting-analysis -c <config.json> --backtest-filename=mystrat_backtest-2022-06-14_16-28-32.json
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --backtest-filename=backtest-result-2025-03-05_20-38-34.zip
|
||||
```
|
||||
|
||||
To use a result from a different results directory, you can use `--backtest-directory` to specify the directory
|
||||
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --backtest-directory custom_results/ --backtest-filename mystrat_backtest-2022-06-14_16-28-32.json
|
||||
freqtrade backtesting-analysis -c <config.json> --backtest-directory custom_results/ --backtest-filename backtest-result-2025-03-05_20-38-34.zip
|
||||
```
|
||||
|
||||
### Tuning the buy tags and sell tags to display
|
||||
@@ -85,7 +84,7 @@ To show only certain buy and sell tags in the displayed output, use the followin
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss
|
||||
```
|
||||
|
||||
@@ -96,7 +95,7 @@ values present on signal candles to allow fine-grained investigation and tuning
|
||||
indicators. To print out a column for a given set of indicators, use the `--indicator-list`
|
||||
option:
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss --indicator-list rsi rsi_1h bb_lowerband ema_9 macd macdsignal
|
||||
```
|
||||
|
||||
@@ -108,24 +107,24 @@ output.
|
||||
The indicator values will be displayed for both entry and exit points. If `--indicator-list all` is specified,
|
||||
only the indicators at the entry point will be shown to avoid excessively large lists, which could occur depending on the strategy.
|
||||
|
||||
There are a range of candle and trade-related fields that are included in the analysis so are
|
||||
There are a range of candle and trade-related fields that are included in the analysis so are
|
||||
automatically accessible by including them on the indicator-list, and these include:
|
||||
|
||||
- **open_date :** trade open datetime
|
||||
- **close_date :** trade close datetime
|
||||
- **min_rate :** minimum price seen throughout the position
|
||||
- **max_rate :** maximum price seen throughout the position
|
||||
- **open :** signal candle open price
|
||||
- **close :** signal candle close price
|
||||
- **high :** signal candle high price
|
||||
- **low :** signal candle low price
|
||||
- **volume :** signal candle volume
|
||||
- **profit_ratio :** trade profit ratio
|
||||
- **profit_abs :** absolute profit return of the trade
|
||||
* **open_date :** trade open datetime
|
||||
* **close_date :** trade close datetime
|
||||
* **min_rate :** minimum price seen throughout the position
|
||||
* **max_rate :** maximum price seen throughout the position
|
||||
* **open :** signal candle open price
|
||||
* **close :** signal candle close price
|
||||
* **high :** signal candle high price
|
||||
* **low :** signal candle low price
|
||||
* **volume :** signal candle volume
|
||||
* **profit_ratio :** trade profit ratio
|
||||
* **profit_abs :** absolute profit return of the trade
|
||||
|
||||
#### Sample Output for Indicator Values
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen
|
||||
```
|
||||
|
||||
@@ -158,13 +157,13 @@ The `--indicator-list` option, by default, displays indicator values for both en
|
||||
|
||||
Example: Display indicator values at entry signals:
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen --entry-only
|
||||
```
|
||||
|
||||
Example: Display indicator values at exit signals:
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen --exit-only
|
||||
```
|
||||
|
||||
@@ -181,7 +180,7 @@ To show only trades between dates within your backtested timerange, supply the u
|
||||
|
||||
For example, if your backtest timerange was `20220101-20221231` but you only want to output trades in January:
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
||||
```
|
||||
|
||||
@@ -189,7 +188,7 @@ freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
||||
|
||||
Use the `--rejected-signals` option to print out rejected signals.
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --rejected-signals
|
||||
```
|
||||
|
||||
@@ -198,13 +197,13 @@ freqtrade backtesting-analysis -c <config.json> --rejected-signals
|
||||
Some of the tabular outputs can become large, so printing them out to the terminal is not preferable.
|
||||
Use the `--analysis-to-csv` option to disable printing out of tables to standard out and write them to CSV files.
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv
|
||||
```
|
||||
|
||||
By default this will write one file per output table you specified in the `backtesting-analysis` command, e.g.
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --rejected-signals --analysis-groups 0 1
|
||||
```
|
||||
|
||||
@@ -216,6 +215,6 @@ This will write to `user_data/backtest_results`:
|
||||
|
||||
To override where the files will be written, also specify the `--analysis-csv-path` option.
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --analysis-csv-path another/data/path/
|
||||
```
|
||||
|
||||
@@ -133,7 +133,7 @@ class MyAwesomeStrategy(IStrategy):
|
||||
]
|
||||
|
||||
# Define a custom max_open_trades space
|
||||
def max_open_trades_space(self) -> List[Dimension]:
|
||||
def max_open_trades_space() -> List[Dimension]:
|
||||
return [
|
||||
Integer(-1, 10, name='max_open_trades'),
|
||||
]
|
||||
@@ -142,7 +142,7 @@ class MyAwesomeStrategy(IStrategy):
|
||||
!!! Note
|
||||
All overrides are optional and can be mixed/matched as necessary.
|
||||
|
||||
### Dynamic parameters
|
||||
## Dynamic parameters
|
||||
|
||||
Parameters can also be defined dynamically, but must be available to the instance once the [`bot_start()` callback](strategy-callbacks.md#bot-start) has been called.
|
||||
|
||||
@@ -159,7 +159,7 @@ class MyAwesomeStrategy(IStrategy):
|
||||
!!! Warning
|
||||
Parameters created this way will not show up in the `list-strategies` parameter count.
|
||||
|
||||
### Overriding Base estimator
|
||||
## Overriding Base estimator
|
||||
|
||||
You can define your own optuna sampler for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass.
|
||||
|
||||
@@ -208,7 +208,6 @@ Some research will be necessary to find additional Samplers (from optunahub) for
|
||||
|
||||
Obviously the same approach will work for all other Samplers optuna supports.
|
||||
|
||||
|
||||
## Space options
|
||||
|
||||
For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types:
|
||||
|
||||
@@ -73,7 +73,7 @@ services:
|
||||
volumes:
|
||||
- "./user_data:/freqtrade/user_data"
|
||||
# Expose api on port 8080 (localhost only)
|
||||
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
||||
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
|
||||
# before enabling this.
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
@@ -100,7 +100,7 @@ services:
|
||||
volumes:
|
||||
- "./user_data:/freqtrade/user_data"
|
||||
# Expose api on port 8080 (localhost only)
|
||||
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
||||
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
|
||||
# before enabling this.
|
||||
ports:
|
||||
- "127.0.0.1:8081:8080"
|
||||
|
||||
@@ -64,18 +64,15 @@ options:
|
||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||
Provide a space-separated list of strategies to
|
||||
backtest. Please note that timeframe needs to be set
|
||||
either in config or via command line. When using this
|
||||
together with `--export trades`, the strategy-name is
|
||||
injected into the filename (so `backtest-data.json`
|
||||
becomes `backtest-data-SampleStrategy.json`
|
||||
either in config or via command line.
|
||||
--export {none,trades,signals}
|
||||
Export backtest results (default: trades).
|
||||
--backtest-filename, --export-filename PATH
|
||||
Use this filename for backtest results.Example:
|
||||
`--backtest-
|
||||
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||
Assumes either `user_data/backtest_results/` or
|
||||
`--export-directory` as base directory.
|
||||
DEPRECATED: This option is deprecated for backtesting
|
||||
and will be removed in a future release. Using a
|
||||
custom filename for backtest results is no longer
|
||||
supported. Use `--backtest-directory` to specify the
|
||||
directory.
|
||||
--backtest-directory, --export-directory PATH
|
||||
Directory to use for backtest results. Example:
|
||||
`--export-directory=user_data/backtest_results/`.
|
||||
|
||||
@@ -62,10 +62,7 @@ options:
|
||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||
Provide a space-separated list of strategies to
|
||||
backtest. Please note that timeframe needs to be set
|
||||
either in config or via command line. When using this
|
||||
together with `--export trades`, the strategy-name is
|
||||
injected into the filename (so `backtest-data.json`
|
||||
becomes `backtest-data-SampleStrategy.json`
|
||||
either in config or via command line.
|
||||
--export {none,trades,signals}
|
||||
Export backtest results (default: trades).
|
||||
--backtest-filename, --export-filename PATH
|
||||
|
||||
@@ -10,10 +10,7 @@ options:
|
||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||
Provide a space-separated list of strategies to
|
||||
backtest. Please note that timeframe needs to be set
|
||||
either in config or via command line. When using this
|
||||
together with `--export trades`, the strategy-name is
|
||||
injected into the filename (so `backtest-data.json`
|
||||
becomes `backtest-data-SampleStrategy.json`
|
||||
either in config or via command line.
|
||||
--strategy-path PATH Specify additional strategy lookup path.
|
||||
--recursive-strategy-search
|
||||
Recursively search for a strategy in the strategies
|
||||
|
||||
+2
-2
@@ -120,8 +120,8 @@ The script below should serve as an example - you may need to adjust the timefra
|
||||
|
||||
``` bash
|
||||
# Cleanup no longer needed data
|
||||
rm user_data/data/<exchange>/futures/*-mark-*
|
||||
rm user_data/data/<exchange>/futures/*-funding_rate-*
|
||||
rm user_data/data/<exchange>/futures/*-mark*
|
||||
rm user_data/data/<exchange>/futures/*-funding_rate*
|
||||
|
||||
# download new data (only required once to fix the mark and funding fee data)
|
||||
freqtrade download-data -t 1h --trading-mode futures --candle-types funding_rate mark [...] --timerange <full timerange you've got other data for>
|
||||
|
||||
+2
-3
@@ -432,7 +432,6 @@ freqtrade download-data --timerange 20250625-20250801 --config tests/testdata/co
|
||||
freqtrade backtesting --config tests/testdata/config.tests.usdt.json -s SampleStrategy --userdir user_data_bttest/ --cache none --timerange 20250701-20250801
|
||||
```
|
||||
|
||||
|
||||
## Continuous integration
|
||||
|
||||
This documents some decisions taken for the CI Pipeline.
|
||||
@@ -464,10 +463,10 @@ git checkout -b new_release <commitid>
|
||||
Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these.
|
||||
|
||||
* Merge the release branch (stable) into this branch.
|
||||
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi.
|
||||
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2025.7` for July 2025). Minor versions can be `2025.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi.
|
||||
* Commit this part.
|
||||
* Push that branch to the remote and create a PR against the **stable branch**.
|
||||
* Update develop version to next version following the pattern `2019.8-dev`.
|
||||
* Update develop version to next version following the pattern `2025.8-dev`.
|
||||
|
||||
### Create changelog from git commits
|
||||
|
||||
|
||||
+34
-3
@@ -239,7 +239,7 @@ Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force)
|
||||
|
||||
### Kucoin Blacklists
|
||||
|
||||
For Kucoin, it is suggested to add `"KCS/<STAKE>"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `KCS` on the account or unless you're willing to disable using `KCS` for fees.
|
||||
For Kucoin, it is suggested to add `"KCS/<STAKE>"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `KCS` on the account or unless you're willing to disable using `KCS` for fees.
|
||||
Kucoin accounts may use `KCS` for fees, and if a trade happens to be on `KCS`, further trades may consume this position and make the initial `KCS` trade unsellable as the expected amount is not there anymore.
|
||||
|
||||
## HTX
|
||||
@@ -319,7 +319,6 @@ API Keys for live futures trading must have the following permissions:
|
||||
|
||||
We do strongly recommend to limit all API keys to the IP you're going to use it from.
|
||||
|
||||
|
||||
## Bitmart
|
||||
|
||||
Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret.
|
||||
@@ -428,6 +427,38 @@ Your balance and trades will now be used from your vault / subaccount - and no l
|
||||
|
||||
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
|
||||
|
||||
### HIP-3 DEXes
|
||||
|
||||
Hyperliquid supports HIP-3 decentralized exchanges (DEXes), which are independent exchanges built on top of the Hyperliquid infrastructure.
|
||||
These DEXes operate similarly to the main Hyperliquid exchange but are community-created and managed.
|
||||
|
||||
To trade on HIP-3 DEXes with Freqtrade, you need to add them to your configuration using the `hip3_dexes` parameter:
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "hyperliquid",
|
||||
"walletAddress": "your_master_wallet_address",
|
||||
"privateKey": "your_api_private_key",
|
||||
"hip3_dexes": ["dex_name_1", "dex_name_2"]
|
||||
}
|
||||
```
|
||||
|
||||
Replace `"dex_name_1"` and `"dex_name_2"` with the actual names of the HIP-3 DEXes you want to trade on (e.g. `vntl` and `xyz`).
|
||||
|
||||
!!! Warning "Performance and Rate Limit Impact"
|
||||
Each HIP-3 DEX you add significantly impacts bot performance and rate limits.
|
||||
|
||||
* **Additional API Calls**: For each HIP-3 DEX configured, Freqtrade needs to make additional API calls.
|
||||
* **Rate Limit Pressure**: Additional API calls contribute to Hyperliquid's strict rate limits. With multiple DEXes, you may hit rate limits faster, or rather, slow down bot operations due to enforced delays.
|
||||
|
||||
Please only add HIP-3 DEXes that you actively trade on. Monitor your logs for rate limit warnings or signs of slowed operations, and adjust your configuration accordingly.
|
||||
Different HIP-3 DEXes may also use different quote currencies - so make sure to only add DEXes that are compatible with your stake currency to avoid unnecessary delays.
|
||||
|
||||
!!! Note
|
||||
HIP-3 DEXes share the same wallet and free amount of collateral as your main Hyperliquid account. Trades on different DEXes will affect your overall account balance and margin.
|
||||
|
||||
The pair name for HIP-3 pairs will be slightly different than non HIP-3 pairs. Please use `list-pairs` subcommand to get the correct pair naming for all pairs for the specified dexes.
|
||||
|
||||
## Bitvavo
|
||||
|
||||
If your account is required to use an operatorId, you can set it in the configuration file as follows:
|
||||
@@ -491,5 +522,5 @@ For example, to test the order type `FOK` with Kraken, and modify candle limit t
|
||||
|
||||
!!! Warning
|
||||
Please make sure to fully understand the impacts of these settings before modifying them.
|
||||
Using `_ft_has_params` overrides may lead to unexpected behavior, and may even break your bot.
|
||||
Using `_ft_has_params` overrides may lead to unexpected behavior, and may even break your bot.
|
||||
We will not be able to provide support for issues caused by custom settings in `_ft_has_params`.
|
||||
|
||||
+8
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
## Supported Markets
|
||||
|
||||
Freqtrade supports spot trading, as well as (isolated) futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an up-to-date list of supported exchanges.
|
||||
Freqtrade supports spot trading, as well as futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an up-to-date list of supported exchanges.
|
||||
|
||||
### Can my bot open short positions?
|
||||
|
||||
@@ -29,6 +29,13 @@ You can however use the [`adjust_trade_position()` callback](strategy-callbacks.
|
||||
|
||||
Backtesting provides an option for this in `--eps` - however this is only there to highlight "hidden" signals, and will not work in live.
|
||||
|
||||
### Does freqtrade support sandbox accounts?
|
||||
|
||||
No, but you can use dry-run mode to simulate trading without risking real funds.
|
||||
|
||||
Sandbox markets are separate, simulated markets - which are not suitable to test your strategy in a realistic environment.
|
||||
These markets usually have different order books, liquidity and trading behaviour (usually with very few participants) - which makes them unsuitable for realistic tests of your strategy.
|
||||
|
||||
### The bot does not start
|
||||
|
||||
Running the bot with `freqtrade trade --config config.json` shows the output `freqtrade: command not found`.
|
||||
|
||||
@@ -45,7 +45,7 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from
|
||||
|
||||
More details about feature engineering available:
|
||||
|
||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
||||
https://www.freqtrade.io/en/stable/freqai-feature-engineering
|
||||
|
||||
:param df: strategy dataframe which will receive the targets
|
||||
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, FreqAI aims to be a sandbox for easily deploying robust machine learning libraries on real-time data ([details](#freqai-position-in-open-source-machine-learning-landscape)).
|
||||
|
||||
!!! Note
|
||||
FreqAI is, and always will be, a not-for-profit, open source project. FreqAI does *not* have a crypto token, FreqAI does *not* sell signals, and FreqAI does not have a domain besides the present [freqtrade documentation](https://www.freqtrade.io/en/latest/freqai/).
|
||||
FreqAI is, and always will be, a not-for-profit, open source project. FreqAI does *not* have a crypto token, FreqAI does *not* sell signals, and FreqAI does not have a domain besides the present [freqtrade documentation](https://www.freqtrade.io/en/stable/freqai/).
|
||||
|
||||
Features include:
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - t
|
||||
```jsonc
|
||||
{
|
||||
//...
|
||||
"jwt_secret_key": "somethingrandom",
|
||||
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||
"CORS_origins": ["https://frequi.freqtrade.io"],
|
||||
//...
|
||||
}
|
||||
@@ -29,7 +29,7 @@ The correct configuration for this case is `http://localhost:8080` - the main pa
|
||||
```jsonc
|
||||
{
|
||||
//...
|
||||
"jwt_secret_key": "somethingrandom",
|
||||
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||
"CORS_origins": ["http://localhost:8080"],
|
||||
//...
|
||||
}
|
||||
|
||||
@@ -20,15 +20,15 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
|
||||
|
||||
### Common settings to all Protections
|
||||
|
||||
| Parameter| Description |
|
||||
|------------|-------------|
|
||||
| `method` | Protection name to use. <br> **Datatype:** String, selected from [available Protections](#available-protections)
|
||||
| `stop_duration_candles` | For how many candles should the lock be set? <br> **Datatype:** Positive integer (in candles)
|
||||
| `stop_duration` | how many minutes should protections be locked. <br>Cannot be used together with `stop_duration_candles`. <br> **Datatype:** Float (in minutes)
|
||||
| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections. <br> **Datatype:** Positive integer (in candles).
|
||||
| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered. <br>Cannot be used together with `lookback_period_candles`. <br>This setting may be ignored by some Protections. <br> **Datatype:** Float (in minutes)
|
||||
| `trade_limit` | Number of trades required at minimum (not used by all Protections). <br> **Datatype:** Positive integer
|
||||
| `unlock_at` | Time when trading will be unlocked regularly (not used by all Protections). <br> **Datatype:** string <br>**Input Format:** "HH:MM" (24-hours)
|
||||
| Parameter | Description |
|
||||
| --------- | ---------- |
|
||||
| `method` | Protection name to use. <br> **Datatype:** String, selected from [available Protections](#available-protections) |
|
||||
| `stop_duration_candles` | For how many candles should the lock be set? <br> **Datatype:** Positive integer (in candles) |
|
||||
| `stop_duration` | how many minutes should protections be locked. <br>Cannot be used together with `stop_duration_candles`. <br> **Datatype:** Float (in minutes) |
|
||||
| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections. <br> **Datatype:** Positive integer (in candles). |
|
||||
| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered. <br>Cannot be used together with `lookback_period_candles`. <br>This setting may be ignored by some Protections. <br> **Datatype:** Float (in minutes) |
|
||||
| `trade_limit` | Number of trades required at minimum (not used by all Protections). <br> **Datatype:** Positive integer |
|
||||
| `unlock_at` | Time when trading will be unlocked regularly (not used by all Protections). <br> **Datatype:** string <br>**Input Format:** "HH:MM" (24-hours) |
|
||||
|
||||
!!! Note "Durations"
|
||||
Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles).
|
||||
@@ -69,7 +69,17 @@ def protections(self):
|
||||
|
||||
#### MaxDrawdown
|
||||
|
||||
`MaxDrawdown` uses all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after the last trade - assuming that the bot needs some time to let markets recover.
|
||||
The `MaxDrawdown` protection evaluates trades that closed within the current `lookback_period` (or `lookback_period_candles`).
|
||||
It supports 2 calculation modes:
|
||||
|
||||
- `calculation_mode: "ratios"` (default): Legacy approximation based on cumulative profit ratios.
|
||||
- `calculation_mode: "equity"`: Standard peak-to-trough drawdown on the account equity curve, using starting balance and cumulative absolute profit.
|
||||
|
||||
With `calculation_mode: "ratios"`, drawdown is derived from cumulative trade profit ratios, not from the account equity curve. This is kept for backward compatibility and can differ from account-level drawdown when position sizing changes over time.
|
||||
|
||||
For new setups, `calculation_mode: "equity"` is recommended. Prefer `calculation_mode: "ratios"` only when you intentionally rely on legacy behavior, especially with fixed stake amount configurations where ratio-based behavior is easier to reason about.
|
||||
|
||||
If the observed drawdown exceeds `max_allowed_drawdown`, trading will stop for `stop_duration` after the last trade - assuming that the bot needs some time to let markets recover.
|
||||
|
||||
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
|
||||
|
||||
@@ -79,6 +89,7 @@ def protections(self):
|
||||
return [
|
||||
{
|
||||
"method": "MaxDrawdown",
|
||||
"calculation_mode": "equity",
|
||||
"lookback_period_candles": 48,
|
||||
"trade_limit": 20,
|
||||
"stop_duration_candles": 12,
|
||||
@@ -160,6 +171,7 @@ class AwesomeStrategy(IStrategy)
|
||||
},
|
||||
{
|
||||
"method": "MaxDrawdown",
|
||||
"calculation_mode": "equity",
|
||||
"lookback_period_candles": 48,
|
||||
"trade_limit": 20,
|
||||
"stop_duration_candles": 4,
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
## Highlighted changes
|
||||
# Highlighted changes
|
||||
|
||||
- ...
|
||||
|
||||
### How to update
|
||||
## How to update
|
||||
|
||||
As always, you can update your bot using one of the following commands:
|
||||
|
||||
#### docker-compose
|
||||
### docker-compose
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### Installation via setup script
|
||||
### Installation via setup script
|
||||
|
||||
```
|
||||
``` bash
|
||||
# Deactivate venv and run
|
||||
./setup.sh --update
|
||||
```
|
||||
|
||||
#### Plain native installation
|
||||
### Plain native installation
|
||||
|
||||
```
|
||||
``` bash
|
||||
git pull
|
||||
pip install -U -r requirements.txt
|
||||
```
|
||||
|
||||
+6
-2
@@ -2,7 +2,9 @@
|
||||
|
||||
[](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
|
||||
[](https://doi.org/10.21105/joss.04864)
|
||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||
[](https://codecov.io/gh/freqtrade/freqtrade)
|
||||
[](https://www.freqtrade.io)
|
||||
[](https://discord.gg/p7nuUNVfP7)
|
||||
|
||||
<!-- GitHub action buttons -->
|
||||
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }
|
||||
@@ -37,6 +39,8 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
||||
|
||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||
|
||||
### Supported Spot Exchanges
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [BingX](https://bingx.com/invite/0EM9RX)
|
||||
- [X] [Bitget](https://www.bitget.com/)
|
||||
@@ -50,7 +54,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
||||
- [X] [MyOKX](https://okx.com/) (OKX EEA)
|
||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||
|
||||
### Supported Futures Exchanges (experimental)
|
||||
### Supported Futures Exchanges
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Bitget](https://www.bitget.com/)
|
||||
|
||||
+57
-18
@@ -9,15 +9,25 @@ The freqtrade documentation describes various ways to install freqtrade
|
||||
* [Manual Installation](#manual-installation)
|
||||
* [Installation with Conda](#installation-with-conda)
|
||||
|
||||
Please consider using the prebuilt [docker images](docker_quickstart.md) to get started quickly while evaluating how freqtrade works.
|
||||
Please consider using the prebuilt [docker images](docker_quickstart.md) to get started quickly.
|
||||
|
||||
!!! Note "Updating"
|
||||
Keeping freqtrade updated is important to [ensure ongoing compatibility](updating.md#why-update) with exchange API's.
|
||||
Please refer to the [updating guide](updating.md) for details on how to update your installation.
|
||||
|
||||
!!! Note "Windows users"
|
||||
We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure).
|
||||
|
||||
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu/Linux instructions will work.
|
||||
If you really want to install freqtrade natively on Windows, best use the [`./setup.ps1` installation script](#use-setupps1-windows).
|
||||
|
||||
Please also make sure to use the 64bit version of Python, as 32bit versions have severe memory limitations, which can negatively impact your experience with backtesting/hyperopt.
|
||||
|
||||
------
|
||||
|
||||
## Information
|
||||
|
||||
For Windows installation, please use the [windows installation guide](windows_installation.md).
|
||||
|
||||
The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the `./setup.sh` script, if it's available for your platform.
|
||||
The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the `./setup.sh` (`./setup.ps1` for Windows) script, if it's available for your platform.
|
||||
|
||||
!!! Note "Version considerations"
|
||||
When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests).
|
||||
@@ -152,20 +162,9 @@ If you are on Debian, Ubuntu or MacOS, freqtrade provides the script to install
|
||||
./setup.sh -i
|
||||
```
|
||||
|
||||
### Activate your virtual environment
|
||||
#### Other options of /setup.sh script
|
||||
|
||||
Each time you open a new terminal, you must run `source .venv/bin/activate` to activate your virtual environment.
|
||||
|
||||
```bash
|
||||
# activate virtual environment
|
||||
source ./.venv/bin/activate
|
||||
```
|
||||
|
||||
[You are now ready](#you-are-ready) to run the bot.
|
||||
|
||||
### Other options of /setup.sh script
|
||||
|
||||
You can as well update, configure and reset the codebase of your bot with `./script.sh`
|
||||
You can also update, configure and reset the codebase of your bot with `./setup.sh`
|
||||
|
||||
```bash
|
||||
# --update, Command git pull to update.
|
||||
@@ -194,6 +193,34 @@ This option will pull the last version of your current branch and update your vi
|
||||
This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv.
|
||||
```
|
||||
|
||||
#### Activate your virtual environment
|
||||
|
||||
Each time you open a new terminal, you must run `source .venv/bin/activate` to activate your virtual environment.
|
||||
|
||||
```bash
|
||||
# activate virtual environment
|
||||
source ./.venv/bin/activate
|
||||
```
|
||||
|
||||
### Use ./setup.ps1 (Windows)
|
||||
|
||||
The script will ask you a few questions to determine which parts should be installed.
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -ExecutionPolicy Bypass
|
||||
cd freqtrade
|
||||
. .\setup.ps1
|
||||
```
|
||||
|
||||
#### Activate your virtual environment (Windows)
|
||||
|
||||
```powershell
|
||||
# activate virtual environment
|
||||
. .\.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
[You are now ready](#you-are-ready) to run the bot.
|
||||
|
||||
-----
|
||||
|
||||
## Manual Installation
|
||||
@@ -337,7 +364,7 @@ conda deactivate
|
||||
|
||||
Happy trading!
|
||||
|
||||
-----
|
||||
------
|
||||
|
||||
## You are ready
|
||||
|
||||
@@ -394,3 +421,15 @@ open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10
|
||||
```
|
||||
|
||||
If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details.
|
||||
|
||||
### Windows Installation error
|
||||
|
||||
```bash
|
||||
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
|
||||
```
|
||||
|
||||
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||
|
||||
You can download the Visual C++ build tools from [the Visual Studio website](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and install "Desktop development with C++" in it's default configuration. Unfortunately, this is a heavy download / dependency so you might want to consider WSL2 or [docker compose](docker_quickstart.md) first.
|
||||
|
||||

|
||||
|
||||
+1
-4
@@ -1,8 +1,5 @@
|
||||
# Trading with Leverage
|
||||
|
||||
!!! Warning "Beta feature"
|
||||
This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord or via Github Issue.
|
||||
|
||||
!!! Note "Multiple bots on one account"
|
||||
You can't run 2 bots on the same account with leverage. For leveraged / margin trading, freqtrade assumes it's the only user of the account, and all liquidation levels are calculated based on this assumption.
|
||||
|
||||
@@ -55,7 +52,7 @@ Perpetual swaps (also known as Perpetual Futures) are contracts traded at a pric
|
||||
In addition to the gains/losses from the change in price of the futures contract, traders also exchange _funding fees_, which are gains/losses worth an amount that is derived from the difference in price between the futures contract and the underlying asset. The difference in price between a futures contract and the underlying asset varies between exchanges.
|
||||
|
||||
To trade in futures markets, you'll have to set `trading_mode` to "futures".
|
||||
You will also have to pick a "margin mode" (explanation below) - with freqtrade currently only supporting isolated margin.
|
||||
You will also have to pick a "margin mode" (explanation below).
|
||||
|
||||
``` json
|
||||
"trading_mode": "futures",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
markdown==3.10
|
||||
markdown==3.10.2
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.7.0
|
||||
mkdocs-material==9.7.1
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.19.1
|
||||
pymdown-extensions==10.21
|
||||
jinja2==3.1.6
|
||||
mike==2.1.3
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@ Sample configuration:
|
||||
"listen_port": 8080,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "somethingrandom",
|
||||
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||
"CORS_origins": [],
|
||||
"username": "Freqtrader",
|
||||
"password": "SuperSecret1!",
|
||||
@@ -56,7 +56,7 @@ secrets.token_hex()
|
||||
|
||||
!!! Danger "Password selection"
|
||||
Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
|
||||
Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!).
|
||||
Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!). This value should also be 32 characters or longer to be safe.
|
||||
|
||||
### Configuration with docker
|
||||
|
||||
@@ -245,7 +245,7 @@ You would then add that token under `ws_token` in your `api_server` config. Like
|
||||
"listen_port": 8080,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "somethingrandom",
|
||||
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||
"CORS_origins": [],
|
||||
"username": "Freqtrader",
|
||||
"password": "SuperSecret1!",
|
||||
|
||||
@@ -225,7 +225,7 @@ class AwesomeStrategy(IStrategy):
|
||||
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns the initial stoploss value.
|
||||
Only called when use_custom_stoploss is set to True.
|
||||
@@ -805,7 +805,7 @@ class AwesomeStrategy(IStrategy):
|
||||
Timing for this function is critical, so avoid doing heavy computations or
|
||||
network requests in this method.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns True (always confirming).
|
||||
|
||||
@@ -853,7 +853,7 @@ class AwesomeStrategy(IStrategy):
|
||||
Timing for this function is critical, so avoid doing heavy computations or
|
||||
network requests in this method.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns True (always confirming).
|
||||
|
||||
@@ -991,7 +991,7 @@ class DigDeeperStrategy(IStrategy):
|
||||
This means extra entry or exit orders with additional fees.
|
||||
Only called when `position_adjustment_enable` is set to True.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns None
|
||||
|
||||
@@ -1118,7 +1118,7 @@ class AwesomeStrategy(IStrategy):
|
||||
This only executes when a order was already placed, still open (unfilled fully or partially)
|
||||
and not timed out on subsequent candles after entry trigger.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
|
||||
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-callbacks/
|
||||
|
||||
When not implemented by a strategy, returns current_order_rate as default.
|
||||
If current_order_rate is returned then the existing order is maintained.
|
||||
@@ -1292,6 +1292,23 @@ Currently two types of annotations are supported, `area` and `line`.
|
||||
}
|
||||
```
|
||||
|
||||
#### Point
|
||||
|
||||
``` json
|
||||
{
|
||||
"type": "point", // Type of the annotation, currently only "point" is supported
|
||||
"x": "2024-01-01 15:00:00", // Start date of the point
|
||||
"y": 94000.2, // Price / y axis value
|
||||
"color": "",
|
||||
"z_level": 5, // z-level, higher values are drawn on top of lower values. Positions relative to the Chart elements need to be set in freqUI.
|
||||
"label": "some label",
|
||||
"size": 2, // Optional, line width in pixels. Defaults to 10
|
||||
"shape": "circle", // Optional, can be "circle", "rect", "roundRect", "triangle", "pin", "arrow", "none".
|
||||
"rotate": 0, // Optional, rotation of the shape/symbol in degrees. Defaults to 0
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
The below example will mark the chart with areas for the hours 8 and 15, with a grey color, highlighting the market open and close hours.
|
||||
This is obviously a very basic example.
|
||||
|
||||
@@ -1369,7 +1386,7 @@ Entries will be validated, and won't be passed to the UI if they don't correspon
|
||||
}
|
||||
)
|
||||
elif (start_dt.hour % 2) == 0:
|
||||
price = dataframe.loc[dataframe["date"] == start_dt, ["close"]].mean()
|
||||
price = dataframe.loc[dataframe["date"] == start_dt, "close"].mean()
|
||||
annotations.append(
|
||||
{
|
||||
"type": "area",
|
||||
|
||||
@@ -594,9 +594,9 @@ Features will now expand automatically. As such, the expansion loops, as well as
|
||||
More details on how these config defined parameters accelerate feature engineering
|
||||
in the documentation at:
|
||||
|
||||
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
|
||||
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
|
||||
|
||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
|
||||
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
|
||||
|
||||
:param df: strategy dataframe which will receive the features
|
||||
:param period: period of the indicator - usage example:
|
||||
@@ -657,9 +657,9 @@ Basic features. Make sure to remove the `{pair}` part from your features.
|
||||
More details on how these config defined parameters accelerate feature engineering
|
||||
in the documentation at:
|
||||
|
||||
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
|
||||
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
|
||||
|
||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
|
||||
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
|
||||
|
||||
:param df: strategy dataframe which will receive the features
|
||||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||
@@ -690,7 +690,7 @@ Basic features. Make sure to remove the `{pair}` part from your features.
|
||||
|
||||
More details about feature engineering available:
|
||||
|
||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
||||
https://www.freqtrade.io/en/stable/freqai-feature-engineering
|
||||
|
||||
:param df: strategy dataframe which will receive the features
|
||||
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
|
||||
@@ -713,7 +713,7 @@ Targets now get their own, dedicated method.
|
||||
|
||||
More details about feature engineering available:
|
||||
|
||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
||||
https://www.freqtrade.io/en/stable/freqai-feature-engineering
|
||||
|
||||
:param df: strategy dataframe which will receive the targets
|
||||
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
||||
|
||||
+14
-3
@@ -6,6 +6,12 @@ To update your freqtrade installation, please use one of the below methods, corr
|
||||
Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release.
|
||||
For the develop branch, please follow PR's to avoid being surprised by changes.
|
||||
|
||||
## Why update?
|
||||
|
||||
Keeping your bot updated not only ensures that you have the latest features and improvements, but is a requirement to keep your bot running smoothly.
|
||||
Freqtrade is heavily dependent on the underlying exchange API's, which change pretty frequently if considered across exchanges.
|
||||
To ensure ongoing compatibility, please make sure to update your bot regularly.
|
||||
|
||||
## Docker
|
||||
|
||||
!!! Note "Legacy installations using the `master` image"
|
||||
@@ -38,7 +44,12 @@ pip install -e .
|
||||
freqtrade install-ui
|
||||
```
|
||||
|
||||
### Problems updating
|
||||
## Problems updating
|
||||
|
||||
Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from updated dependencies, which fail to install (for example TA-lib).
|
||||
Please refer to the corresponding installation sections (common problems linked below)
|
||||
Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from dependencies which fail to install.
|
||||
We try to make sure that heavy dependencies have wheels available for major platforms, but sometimes this is not possible.
|
||||
|
||||
Please refer to the corresponding installation sections (common problem sections linked below).
|
||||
|
||||
[Common installation problems](installation.md#troubleshooting)
|
||||
[Common installation problems - windows](installation.md#windows-installation-error)
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# Windows installation
|
||||
|
||||
We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure).
|
||||
|
||||
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
||||
Otherwise, please follow the instructions below.
|
||||
|
||||
All instructions assume that python 3.11+ is installed and available.
|
||||
|
||||
## Clone the git repository
|
||||
|
||||
First of all clone the repository by running:
|
||||
|
||||
``` powershell
|
||||
git clone https://github.com/freqtrade/freqtrade.git
|
||||
```
|
||||
|
||||
Now, choose your installation method, either automatically via script (recommended) or manually following the corresponding instructions.
|
||||
|
||||
## Install freqtrade automatically
|
||||
|
||||
### Run the installation script
|
||||
|
||||
The script will ask you a few questions to determine which parts should be installed.
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -ExecutionPolicy Bypass
|
||||
cd freqtrade
|
||||
. .\setup.ps1
|
||||
```
|
||||
|
||||
## Install freqtrade manually
|
||||
|
||||
!!! Note "64bit Python version"
|
||||
Please make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows.
|
||||
32bit python versions are no longer supported under Windows.
|
||||
|
||||
!!! Hint
|
||||
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
|
||||
|
||||
|
||||
### Error during installation on Windows
|
||||
|
||||
``` bash
|
||||
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
|
||||
```
|
||||
|
||||
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||
|
||||
You can download the Visual C++ build tools from [here](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and install "Desktop development with C++" in it's default configuration. Unfortunately, this is a heavy download / dependency so you might want to consider WSL2 or [docker compose](docker_quickstart.md) first.
|
||||
|
||||

|
||||
|
||||
---
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2025.12"
|
||||
__version__ = "2026.2"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -103,7 +103,13 @@ ARGS_BACKTEST_SHOW = [
|
||||
"backtest_breakdown",
|
||||
]
|
||||
|
||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"]
|
||||
ARGS_LIST_EXCHANGES = [
|
||||
"print_one_column",
|
||||
"list_exchanges_all",
|
||||
"trading_mode",
|
||||
"dex_exchanges",
|
||||
"list_exchanges_futures_options",
|
||||
]
|
||||
|
||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column", "trading_mode"]
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Definition of cli arguments used in arguments.py
|
||||
"""
|
||||
|
||||
from argparse import ArgumentTypeError
|
||||
from argparse import SUPPRESS, ArgumentTypeError
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.constants import (
|
||||
@@ -215,9 +215,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"--strategy-list",
|
||||
help="Provide a space-separated list of strategies to backtest. "
|
||||
"Please note that timeframe needs to be set either in config "
|
||||
"or via command line. When using this together with `--export trades`, "
|
||||
"the strategy-name is injected into the filename "
|
||||
"(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`",
|
||||
"or via command line. ",
|
||||
nargs="+",
|
||||
),
|
||||
"backtest_notes": Arg(
|
||||
@@ -240,6 +238,14 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"exportfilename": Arg(
|
||||
"--backtest-filename",
|
||||
"--export-filename",
|
||||
fthelp={
|
||||
"freqtrade backtesting": (
|
||||
"DEPRECATED: This option is deprecated for backtesting and will be removed "
|
||||
"in a future release. "
|
||||
"Using a custom filename for backtest results is no longer supported. "
|
||||
"Use `--backtest-directory` to specify the directory."
|
||||
),
|
||||
},
|
||||
help="Use this filename for backtest results."
|
||||
"Example: `--backtest-filename=backtest_results_2020-09-27_16-20-48.json`. "
|
||||
"Assumes either `user_data/backtest_results/` or `--export-directory` as base directory.",
|
||||
@@ -388,6 +394,13 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help="Print only DEX exchanges.",
|
||||
action="store_true",
|
||||
),
|
||||
"list_exchanges_futures_options": Arg(
|
||||
"--ccxt-show-futures-options-exchanges",
|
||||
help=SUPPRESS,
|
||||
# Show compatibility with ccxt for futures functionality
|
||||
# Doesn't show in help as it's an internal/debug option.
|
||||
action="store_true",
|
||||
),
|
||||
# List pairs / markets
|
||||
"list_pairs_all": Arg(
|
||||
"-a",
|
||||
|
||||
@@ -223,7 +223,7 @@ def start_list_trades_data(args: dict[str, Any]) -> None:
|
||||
end.strftime(DATETIME_PRINT_FORMAT),
|
||||
str(length),
|
||||
)
|
||||
for pair, start, end, length in sorted(paircombs1, key=lambda x: (x[0]))
|
||||
for pair, start, end, length in sorted(paircombs1, key=lambda x: x[0])
|
||||
],
|
||||
("Pair", "Type", "From", "To", "Trades"),
|
||||
summary=title,
|
||||
|
||||
@@ -13,6 +13,8 @@ def start_convert_db(args: dict[str, Any]) -> None:
|
||||
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.persistence import Order, Trade, init_db
|
||||
from freqtrade.persistence.custom_data import _CustomData
|
||||
from freqtrade.persistence.key_value_store import _KeyValueStoreModel
|
||||
from freqtrade.persistence.migrations import set_sequence_ids
|
||||
from freqtrade.persistence.pairlock import PairLock
|
||||
|
||||
@@ -25,6 +27,8 @@ def start_convert_db(args: dict[str, Any]) -> None:
|
||||
|
||||
trade_count = 0
|
||||
pairlock_count = 0
|
||||
kv_count = 0
|
||||
custom_data_count = 0
|
||||
for trade in Trade.get_trades():
|
||||
trade_count += 1
|
||||
make_transient(trade)
|
||||
@@ -41,16 +45,35 @@ def start_convert_db(args: dict[str, Any]) -> None:
|
||||
session_target.add(pairlock)
|
||||
session_target.commit()
|
||||
|
||||
for kv in _KeyValueStoreModel.session.scalars(select(_KeyValueStoreModel)):
|
||||
kv_count += 1
|
||||
make_transient(kv)
|
||||
session_target.add(kv)
|
||||
session_target.commit()
|
||||
|
||||
for cd in _CustomData.session.scalars(select(_CustomData)):
|
||||
custom_data_count += 1
|
||||
make_transient(cd)
|
||||
session_target.add(cd)
|
||||
session_target.commit()
|
||||
|
||||
# Update sequences
|
||||
max_trade_id = session_target.scalar(select(func.max(Trade.id)))
|
||||
max_order_id = session_target.scalar(select(func.max(Order.id)))
|
||||
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
|
||||
max_kv_id = session_target.scalar(select(func.max(_KeyValueStoreModel.id)))
|
||||
max_custom_data_id = session_target.scalar(select(func.max(_CustomData.id)))
|
||||
|
||||
set_sequence_ids(
|
||||
session_target.get_bind(),
|
||||
trade_id=max_trade_id,
|
||||
order_id=max_order_id,
|
||||
pairlock_id=max_pairlock_id,
|
||||
trade_id=(max_trade_id or 0) + 1,
|
||||
order_id=(max_order_id or 0) + 1,
|
||||
pairlock_id=(max_pairlock_id or 0) + 1,
|
||||
kv_id=(max_kv_id or 0) + 1,
|
||||
custom_data_id=(max_custom_data_id or 0) + 1,
|
||||
)
|
||||
|
||||
logger.info(f"Migrated {trade_count} Trades, and {pairlock_count} Pairlocks.")
|
||||
logger.info(
|
||||
f"Migrated {trade_count} Trades, {pairlock_count} Pairlocks, "
|
||||
f"{kv_count} Key-Value pairs, and {custom_data_count} Custom Data entries."
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import sys
|
||||
from typing import Any
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
||||
from freqtrade.exceptions import ConfigurationError, DependencyException, OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -38,13 +38,15 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
||||
else:
|
||||
available_exchanges = [e for e in available_exchanges if e["valid"] is not False]
|
||||
title = f"Exchanges available for Freqtrade ({len(available_exchanges)} exchanges):"
|
||||
|
||||
show_fut_reasons = args.get("list_exchanges_futures_options", False)
|
||||
table = Table(title=title)
|
||||
|
||||
table.add_column("Exchange Name")
|
||||
table.add_column("Class Name")
|
||||
table.add_column("Markets")
|
||||
table.add_column("Reason")
|
||||
if show_fut_reasons:
|
||||
table.add_column("Futures Reason")
|
||||
|
||||
trading_mode = args.get("trading_mode", None)
|
||||
dex_only = args.get("dex_exchanges", False)
|
||||
@@ -78,12 +80,14 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
||||
if exchange["dex"]:
|
||||
trade_modes = Text("DEX: ") + trade_modes
|
||||
trade_modes.stylize("bold", 0, 3)
|
||||
futcol = [] if not show_fut_reasons else [exchange["comment_futures"]]
|
||||
|
||||
table.add_row(
|
||||
name,
|
||||
classname,
|
||||
trade_modes,
|
||||
exchange["comment"],
|
||||
*futcol,
|
||||
style=None if exchange["valid"] else "red",
|
||||
)
|
||||
# table.add_row(*[exchange[header] for header in headers])
|
||||
@@ -162,7 +166,14 @@ def start_list_strategies(args: dict[str, Any]) -> None:
|
||||
strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
|
||||
for obj in strategy_objs:
|
||||
if obj["class"]:
|
||||
obj["hyperoptable"] = detect_all_parameters(obj["class"])
|
||||
try:
|
||||
obj["hyperoptable"] = detect_all_parameters(obj["class"])
|
||||
except DependencyException as e:
|
||||
logger.warning(
|
||||
f"Cannot detect hyperoptable parameters for strategy {obj['name']}. Reason: {e}"
|
||||
)
|
||||
obj["hyperoptable"] = {}
|
||||
|
||||
else:
|
||||
obj["hyperoptable"] = {}
|
||||
|
||||
|
||||
@@ -752,6 +752,7 @@ CONF_SCHEMA = {
|
||||
"jwt_secret_key": {
|
||||
"description": "Secret key for JWT authentication.",
|
||||
"type": "string",
|
||||
"default": "somethingRandomSomethingRandom123",
|
||||
},
|
||||
"CORS_origins": {
|
||||
"description": "List of allowed CORS origins.",
|
||||
@@ -764,7 +765,14 @@ CONF_SCHEMA = {
|
||||
"enum": ["error", "info"],
|
||||
},
|
||||
},
|
||||
"required": ["enabled", "listen_ip_address", "listen_port", "username", "password"],
|
||||
"required": [
|
||||
"enabled",
|
||||
"listen_ip_address",
|
||||
"listen_port",
|
||||
"username",
|
||||
"password",
|
||||
"jwt_secret_key",
|
||||
],
|
||||
},
|
||||
# end of RPC section
|
||||
"db_url": {
|
||||
|
||||
@@ -221,30 +221,30 @@ class Configuration:
|
||||
config, argname="exportfilename", logstring="Storing backtest results to {} ..."
|
||||
)
|
||||
config["exportfilename"] = Path(config["exportfilename"])
|
||||
if config.get("exportdirectory") and Path(config["exportdirectory"]).is_dir():
|
||||
logger.warning(
|
||||
"DEPRECATED: Using `--export-filename` with directories is deprecated, "
|
||||
"use `--backtest-directory` instead."
|
||||
)
|
||||
if config.get("exportdirectory") is None:
|
||||
# Fallback - assign export-directory directly.
|
||||
config["exportdirectory"] = config["exportfilename"]
|
||||
if config.get("exportfilename"):
|
||||
if Path(config["exportfilename"]).is_dir():
|
||||
logger.warning(
|
||||
"DEPRECATED: Using `--export-filename` with directories is deprecated, "
|
||||
"use `--backtest-directory` instead."
|
||||
)
|
||||
if config.get("exportdirectory") is None:
|
||||
# Fallback - assign export-directory directly.
|
||||
config["exportdirectory"] = config["exportfilename"]
|
||||
elif config.get("runmode") == RunMode.BACKTEST:
|
||||
logger.warning(
|
||||
"DEPRECATED: Using `--export-filename` has no impact when backtesting. "
|
||||
"Please use `--notes` to annotate backtest results and "
|
||||
"`--backtest-directory` to specify the output directory. "
|
||||
)
|
||||
if not config.get("exportdirectory"):
|
||||
config["exportdirectory"] = config["user_data_dir"] / "backtest_results"
|
||||
if not config.get("exportfilename"):
|
||||
config["exportfilename"] = None
|
||||
|
||||
config["exportfilename"] = config.get("exportfilename", None)
|
||||
if config.get("exportfilename"):
|
||||
# ensure exportfilename is a Path object
|
||||
config["exportfilename"] = Path(config["exportfilename"])
|
||||
config["exportdirectory"] = Path(config["exportdirectory"])
|
||||
|
||||
if self.args.get("show_sensitive"):
|
||||
logger.warning(
|
||||
"Sensitive information will be shown in the upcoming 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
|
||||
self._args_to_config(
|
||||
@@ -312,6 +312,13 @@ class Configuration:
|
||||
|
||||
self._process_datadir_options(config)
|
||||
|
||||
if self.args.get("show_sensitive"):
|
||||
logger.warning(
|
||||
"Sensitive information will be shown in the upcoming output. "
|
||||
"Please make sure to never share this output without redacting "
|
||||
"the information yourself."
|
||||
)
|
||||
|
||||
self._args_to_config(
|
||||
config,
|
||||
argname="strategy_list",
|
||||
|
||||
@@ -30,7 +30,7 @@ def log_config_error_range(path: str, errmsg: str) -> str:
|
||||
offsetlist = re.findall(r"(?<=Parse\serror\sat\soffset\s)\d+", errmsg)
|
||||
if offsetlist:
|
||||
offset = int(offsetlist[0])
|
||||
text = Path(path).read_text()
|
||||
text = Path(path).read_text(encoding="utf-8")
|
||||
# Fetch an offset of 80 characters around the error line
|
||||
subtext = text[offset - min(80, offset) : offset + 80]
|
||||
segments = subtext.split("\n")
|
||||
|
||||
@@ -239,3 +239,6 @@ IntOrInf = float
|
||||
|
||||
|
||||
EntryExecuteMode = Literal["initial", "pos_adjust", "replace"]
|
||||
|
||||
# Prefixes for low-priced coins like 1000PEPE/USDDT:USDT or KPEPE/USDC (hyperliquid)
|
||||
PairPrefixes = ["1000", "1000000", "1M", "K"]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from numpy import format_float_positional
|
||||
from pandas import DataFrame, Series
|
||||
|
||||
|
||||
@@ -11,7 +12,10 @@ def get_tick_size_over_time(candles: DataFrame) -> Series:
|
||||
# count the number of significant digits for the open and close prices
|
||||
for col in ["open", "high", "low", "close"]:
|
||||
candles[f"{col}_count"] = (
|
||||
candles[col].round(14).apply("{:.15f}".format).str.extract(r"\.(\d*[1-9])")[0].str.len()
|
||||
candles[col]
|
||||
.apply(format_float_positional, precision=14, unique=False, fractional=False, trim="-")
|
||||
.str.extract(r"\.(\d*[1-9])")[0]
|
||||
.str.len()
|
||||
)
|
||||
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
|
||||
axis=1
|
||||
|
||||
@@ -70,28 +70,6 @@ class IDataHandler(ABC):
|
||||
if match and len(match.groups()) > 1
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> list[str]:
|
||||
"""
|
||||
Returns a list of all pairs with ohlcv data available in this datadir
|
||||
for the specified timeframe
|
||||
:param datadir: Directory to search for ohlcv files
|
||||
:param timeframe: Timeframe to search pairs for
|
||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||
:return: List of Pairs
|
||||
"""
|
||||
candle = ""
|
||||
if candle_type != CandleType.SPOT:
|
||||
datadir = datadir.joinpath("futures")
|
||||
candle = f"-{candle_type}"
|
||||
ext = cls._get_file_extension()
|
||||
_tmp = [
|
||||
re.search(r"^(\S+)(?=\-" + timeframe + candle + f".{ext})", p.name)
|
||||
for p in datadir.glob(f"*{timeframe}{candle}.{ext}")
|
||||
]
|
||||
# Check if regex found something and only return these results
|
||||
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
||||
|
||||
@abstractmethod
|
||||
def ohlcv_store(
|
||||
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
|
||||
|
||||
@@ -334,7 +334,10 @@ def calculate_expectancy(trades: pd.DataFrame) -> tuple[float, float]:
|
||||
|
||||
|
||||
def calculate_sortino(
|
||||
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
|
||||
trades: pd.DataFrame,
|
||||
min_date: datetime | None,
|
||||
max_date: datetime | None,
|
||||
starting_balance: float,
|
||||
) -> float:
|
||||
"""
|
||||
Calculate sortino
|
||||
@@ -362,7 +365,10 @@ def calculate_sortino(
|
||||
|
||||
|
||||
def calculate_sharpe(
|
||||
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
|
||||
trades: pd.DataFrame,
|
||||
min_date: datetime | None,
|
||||
max_date: datetime | None,
|
||||
starting_balance: float,
|
||||
) -> float:
|
||||
"""
|
||||
Calculate sharpe
|
||||
@@ -389,7 +395,10 @@ def calculate_sharpe(
|
||||
|
||||
|
||||
def calculate_calmar(
|
||||
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
|
||||
trades: pd.DataFrame,
|
||||
min_date: datetime | None,
|
||||
max_date: datetime | None,
|
||||
starting_balance: float,
|
||||
) -> float:
|
||||
"""
|
||||
Calculate calmar
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class CandleType(str, Enum):
|
||||
class CandleType(StrEnum):
|
||||
"""Enum to distinguish candle types"""
|
||||
|
||||
SPOT = "spot"
|
||||
@@ -14,9 +14,6 @@ class CandleType(str, Enum):
|
||||
FUNDING_RATE = "funding_rate"
|
||||
# BORROW_RATE = "borrow_rate" # * unimplemented
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
@staticmethod
|
||||
def from_string(value: str) -> "CandleType":
|
||||
if not value:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class MarginMode(str, Enum):
|
||||
class MarginMode(StrEnum):
|
||||
"""
|
||||
Enum to distinguish between
|
||||
cross margin/futures margin_mode and
|
||||
@@ -11,6 +11,3 @@ class MarginMode(str, Enum):
|
||||
CROSS = "cross"
|
||||
ISOLATED = "isolated"
|
||||
NONE = ""
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.value.lower()}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class OrderTypeValues(str, Enum):
|
||||
class OrderTypeValues(StrEnum):
|
||||
limit = "limit"
|
||||
market = "market"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class PriceType(str, Enum):
|
||||
class PriceType(StrEnum):
|
||||
"""Enum to distinguish possible trigger prices for stoplosses"""
|
||||
|
||||
LAST = "last"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class RPCMessageType(str, Enum):
|
||||
class RPCMessageType(StrEnum):
|
||||
STATUS = "status"
|
||||
WARNING = "warning"
|
||||
EXCEPTION = "exception"
|
||||
@@ -25,21 +25,16 @@ class RPCMessageType(str, Enum):
|
||||
NEW_CANDLE = "new_candle"
|
||||
|
||||
def __repr__(self):
|
||||
return self.value
|
||||
|
||||
def __str__(self):
|
||||
# TODO: do we still need to overwrite __repr__? Impact needs to be looked at in detail
|
||||
return self.value
|
||||
|
||||
|
||||
# Enum for parsing requests from ws consumers
|
||||
class RPCRequestType(str, Enum):
|
||||
class RPCRequestType(StrEnum):
|
||||
SUBSCRIBE = "subscribe"
|
||||
|
||||
WHITELIST = "whitelist"
|
||||
ANALYZED_DF = "analyzed_df"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class RunMode(str, Enum):
|
||||
class RunMode(StrEnum):
|
||||
"""
|
||||
Bot running mode (backtest, hyperopt, ...)
|
||||
can be "live", "dry-run", "backtest", "hyperopt".
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class SignalType(Enum):
|
||||
class SignalType(StrEnum):
|
||||
"""
|
||||
Enum to distinguish between enter and exit signals
|
||||
"""
|
||||
@@ -11,11 +11,8 @@ class SignalType(Enum):
|
||||
ENTER_SHORT = "enter_short"
|
||||
EXIT_SHORT = "exit_short"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
|
||||
class SignalTagType(Enum):
|
||||
class SignalTagType(StrEnum):
|
||||
"""
|
||||
Enum for signal columns
|
||||
"""
|
||||
@@ -23,13 +20,7 @@ class SignalTagType(Enum):
|
||||
ENTER_TAG = "enter_tag"
|
||||
EXIT_TAG = "exit_tag"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
|
||||
class SignalDirection(str, Enum):
|
||||
class SignalDirection(StrEnum):
|
||||
LONG = "long"
|
||||
SHORT = "short"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class TradingMode(str, Enum):
|
||||
class TradingMode(StrEnum):
|
||||
"""
|
||||
Enum to distinguish between
|
||||
spot, margin, futures or any other trading method
|
||||
@@ -10,6 +10,3 @@ class TradingMode(str, Enum):
|
||||
SPOT = "spot"
|
||||
MARGIN = "margin"
|
||||
FUTURES = "futures"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
@@ -48,6 +48,7 @@ class Binance(Exchange):
|
||||
"has_delisting": True,
|
||||
}
|
||||
_ft_has_futures: FtHas = {
|
||||
"ohlcv_candle_limit": 499,
|
||||
"funding_fee_candle_limit": 1000,
|
||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
||||
@@ -68,6 +69,7 @@ class Binance(Exchange):
|
||||
"BFUSD": "USDT",
|
||||
},
|
||||
}
|
||||
_can_use_data_download_fast = True
|
||||
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
(TradingMode.SPOT, MarginMode.NONE),
|
||||
@@ -181,7 +183,8 @@ class Binance(Exchange):
|
||||
return DataFrame(columns=DEFAULT_DATAFRAME_COLUMNS)
|
||||
|
||||
if (
|
||||
self._config["exchange"].get("only_from_ccxt", False)
|
||||
not self._can_use_data_download_fast
|
||||
or self._config["exchange"].get("only_from_ccxt", False)
|
||||
or
|
||||
# only download timeframes with significant improvements,
|
||||
# otherwise fall back to rest API
|
||||
@@ -405,7 +408,10 @@ class Binance(Exchange):
|
||||
) -> tuple[str, list[list]]:
|
||||
logger.info(f"Fetching trades for {pair} from Binance, {from_id=}, {since=}, {until=}")
|
||||
|
||||
if not self._config["exchange"].get("only_from_ccxt", False):
|
||||
if (
|
||||
not self._config["exchange"].get("only_from_ccxt", False)
|
||||
and self._can_use_data_download_fast
|
||||
):
|
||||
if from_id is None or not since:
|
||||
trades = await self._api_async.fetch_trades(
|
||||
pair,
|
||||
@@ -569,3 +575,5 @@ class Binanceus(Binance):
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
(TradingMode.SPOT, MarginMode.NONE),
|
||||
]
|
||||
# binance vision does not have data for binanceus
|
||||
_can_use_data_download_fast = False
|
||||
|
||||
+38902
-35472
File diff suppressed because it is too large
Load Diff
@@ -47,14 +47,16 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool:
|
||||
f"{', '.join(available_exchanges())}"
|
||||
)
|
||||
|
||||
valid, reason, _ = validate_exchange(exchange)
|
||||
valid, reason, _, _ = validate_exchange(exchange)
|
||||
if not valid:
|
||||
if check_for_bad:
|
||||
raise OperationalException(
|
||||
f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}'
|
||||
f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}.'
|
||||
)
|
||||
else:
|
||||
logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}')
|
||||
logger.warning(
|
||||
f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}.'
|
||||
)
|
||||
|
||||
if MAP_EXCHANGE_CHILDCLASS.get(exchange, exchange) in SUPPORTED_EXCHANGES:
|
||||
logger.info(
|
||||
|
||||
@@ -36,12 +36,13 @@ API_RETRY_COUNT = 4
|
||||
API_FETCH_ORDER_RETRY_COUNT = 5
|
||||
|
||||
BAD_EXCHANGES = {
|
||||
"bitmex": "Various reasons.",
|
||||
"probit": "Requires additional, regular calls to `signIn()`.",
|
||||
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
|
||||
"kucoinfutures": "Unsupported futures exchange.",
|
||||
"poloniexfutures": "Unsupported futures exchange.",
|
||||
"binancecoinm": "Unsupported futures exchange.",
|
||||
"bitmex": "Various reasons",
|
||||
"probit": "Requires additional, regular calls to `signIn()`",
|
||||
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders",
|
||||
"krakenfutures": "Unsupported futures exchange",
|
||||
"kucoinfutures": "Unsupported futures exchange",
|
||||
"poloniexfutures": "Unsupported futures exchange",
|
||||
"binancecoinm": "Unsupported futures exchange",
|
||||
}
|
||||
|
||||
MAP_EXCHANGE_CHILDCLASS = {
|
||||
@@ -78,31 +79,35 @@ EXCHANGE_HAS_REQUIRED: dict[str, list[str]] = {
|
||||
"fetchOHLCV": [],
|
||||
}
|
||||
|
||||
EXCHANGE_HAS_OPTIONAL = [
|
||||
EXCHANGE_HAS_OPTIONAL: dict[str, list[str]] = {
|
||||
# Private
|
||||
"fetchMyTrades", # Trades for order - fee detection
|
||||
"createLimitOrder",
|
||||
"createMarketOrder", # Either OR for orders
|
||||
# 'setLeverage', # Margin/Futures trading
|
||||
# 'setMarginMode', # Margin/Futures trading
|
||||
# 'fetchFundingHistory', # Futures trading
|
||||
"fetchMyTrades": [], # Trades for order - fee detection
|
||||
"createLimitOrder": [],
|
||||
"createMarketOrder": [], # Either OR for orders
|
||||
# Public
|
||||
"fetchOrderBook",
|
||||
"fetchL2OrderBook",
|
||||
"fetchTicker", # OR for pricing
|
||||
"fetchTickers", # For volumepairlist?
|
||||
"fetchTrades", # Downloading trades data
|
||||
# 'fetchFundingRateHistory', # Futures trading
|
||||
# 'fetchPositions', # Futures trading
|
||||
# 'fetchLeverageTiers', # Futures initialization
|
||||
# 'fetchMarketLeverageTiers', # Futures initialization
|
||||
# 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance...
|
||||
# "fetchPremiumIndexOHLCV", # Futures additional data
|
||||
# "fetchMarkOHLCV", # Futures additional data
|
||||
# "fetchIndexOHLCV", # Futures additional data
|
||||
"fetchOrderBook": [],
|
||||
"fetchL2OrderBook": [],
|
||||
"fetchTicker": [], # OR for pricing
|
||||
"fetchTickers": [], # For volumepairlist?
|
||||
"fetchTrades": [], # Downloading trades data
|
||||
"fetchOrders": ["fetchOpenOrders", "fetchClosedOrders"], # , # Refinding balance...
|
||||
# ccxt.pro
|
||||
"watchOHLCV",
|
||||
]
|
||||
"watchOHLCV": [],
|
||||
}
|
||||
|
||||
EXCHANGE_HAS_OPTIONAL_FUTURES: dict[str, list[str]] = {
|
||||
# private
|
||||
"setLeverage": [], # Margin/Futures trading
|
||||
"setMarginMode": [], # Margin/Futures trading
|
||||
"fetchFundingHistory": [], # Futures trading
|
||||
# Public
|
||||
"fetchFundingRateHistory": [], # Futures trading
|
||||
"fetchPositions": [], # Futures trading
|
||||
"fetchLeverageTiers": ["fetchMarketLeverageTiers"], # Futures initialization
|
||||
"fetchMarkOHLCV": [],
|
||||
"fetchIndexOHLCV": [], # Futures additional data
|
||||
"fetchPremiumIndexOHLCV": [],
|
||||
}
|
||||
|
||||
|
||||
def calculate_backoff(retrycount, max_retries):
|
||||
|
||||
@@ -73,6 +73,7 @@ from freqtrade.exchange.exchange_types import (
|
||||
CcxtPosition,
|
||||
FtHas,
|
||||
FundingRate,
|
||||
LeverageTier,
|
||||
OHLCVResponse,
|
||||
OrderBook,
|
||||
Ticker,
|
||||
@@ -105,7 +106,7 @@ from freqtrade.misc import (
|
||||
file_dump_json,
|
||||
file_load_json,
|
||||
safe_value_fallback,
|
||||
safe_value_fallback2,
|
||||
safe_value_nested,
|
||||
)
|
||||
from freqtrade.util import FtTTLCache, PeriodicCache, dt_from_ts, dt_now
|
||||
from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time
|
||||
@@ -195,29 +196,30 @@ class Exchange:
|
||||
self._exchange_ws: ExchangeWS | None = None
|
||||
self._markets: dict = {}
|
||||
self._trading_fees: dict[str, Any] = {}
|
||||
self._leverage_tiers: dict[str, list[dict]] = {}
|
||||
self._leverage_tiers: dict[str, list[LeverageTier]] = {}
|
||||
# Lock event loop. This is necessary to avoid race-conditions when using force* commands
|
||||
# Due to funding fee fetching.
|
||||
self._loop_lock = Lock()
|
||||
self.loop = self._init_async_loop()
|
||||
self._config: Config = {}
|
||||
self._config: Config = config
|
||||
|
||||
# Leverage properties
|
||||
self.trading_mode: TradingMode = TradingMode(
|
||||
config.get("trading_mode", self._supported_trading_mode_margin_pairs[0][0])
|
||||
self._config.get("trading_mode", self._supported_trading_mode_margin_pairs[0][0])
|
||||
)
|
||||
self.margin_mode: MarginMode = MarginMode(
|
||||
MarginMode(config.get("margin_mode"))
|
||||
if config.get("margin_mode")
|
||||
self._config["margin_mode"]
|
||||
if self._config.get("margin_mode")
|
||||
else self._supported_trading_mode_margin_pairs[0][1]
|
||||
)
|
||||
config["trading_mode"] = self.trading_mode
|
||||
config["margin_mode"] = self.margin_mode
|
||||
config["candle_type_def"] = CandleType.get_default(self.trading_mode)
|
||||
self._config.update(config)
|
||||
self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
|
||||
self._config["trading_mode"] = self.trading_mode
|
||||
self._config["margin_mode"] = self.margin_mode
|
||||
self._config["candle_type_def"] = CandleType.get_default(self.trading_mode)
|
||||
self.liquidation_buffer = self._config.get("liquidation_buffer", 0.05)
|
||||
|
||||
exchange_conf: ExchangeConfig = exchange_config if exchange_config else config["exchange"]
|
||||
exchange_conf: ExchangeConfig = (
|
||||
exchange_config if exchange_config else self._config["exchange"]
|
||||
)
|
||||
|
||||
# Deep merge ft_has with default ft_has options
|
||||
# Must be called before ft_has is used.
|
||||
@@ -248,14 +250,14 @@ class Exchange:
|
||||
# Holds all open sell orders for dry_run
|
||||
self._dry_run_open_orders: dict[str, Any] = {}
|
||||
|
||||
if config["dry_run"]:
|
||||
if self._config["dry_run"]:
|
||||
logger.info("Instance is running with dry_run enabled")
|
||||
logger.info(f"Using CCXT {ccxt.__version__}")
|
||||
|
||||
# Don't remove exchange credentials for dry-run or if always_require_api_keys is set
|
||||
remove_exchange_credentials(
|
||||
exchange_conf,
|
||||
not self._ft_has["always_require_api_keys"] and config.get("dry_run", False),
|
||||
not self._ft_has["always_require_api_keys"] and self._config.get("dry_run", False),
|
||||
)
|
||||
self.log_responses = exchange_conf.get("log_responses", False)
|
||||
|
||||
@@ -296,7 +298,7 @@ class Exchange:
|
||||
if validate:
|
||||
# Initial markets load
|
||||
self.reload_markets(True, load_leverage_tiers=False)
|
||||
self.validate_config(config)
|
||||
self.validate_config(self._config)
|
||||
|
||||
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
||||
self.fill_leverage_tiers()
|
||||
@@ -312,10 +314,19 @@ class Exchange:
|
||||
if self._exchange_ws:
|
||||
self._exchange_ws.cleanup()
|
||||
logger.debug("Exchange object destroyed, closing async loop")
|
||||
try:
|
||||
generic_loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
generic_loop = None
|
||||
loop_running = (getattr(self, "loop", None) and self.loop.is_running()) or (
|
||||
generic_loop is not None and generic_loop.is_running()
|
||||
)
|
||||
|
||||
if (
|
||||
getattr(self, "_api_async", None)
|
||||
and inspect.iscoroutinefunction(self._api_async.close)
|
||||
and self._api_async.session
|
||||
and not loop_running
|
||||
):
|
||||
logger.debug("Closing async ccxt session.")
|
||||
self.loop.run_until_complete(self._api_async.close())
|
||||
@@ -323,6 +334,7 @@ class Exchange:
|
||||
self._ws_async
|
||||
and inspect.iscoroutinefunction(self._ws_async.close)
|
||||
and self._ws_async.session
|
||||
and not loop_running
|
||||
):
|
||||
logger.debug("Closing ws ccxt session.")
|
||||
self.loop.run_until_complete(self._ws_async.close())
|
||||
@@ -480,7 +492,7 @@ class Exchange:
|
||||
def _log_exchange_response(self, endpoint: str, response, *, add_info=None) -> None:
|
||||
"""Log exchange responses"""
|
||||
if self.log_responses:
|
||||
add_info_str = "" if add_info is None else f" {add_info}: "
|
||||
add_info_str = "" if add_info is None else f"{add_info}: "
|
||||
logger.info(f"API {endpoint}: {add_info_str}{response}")
|
||||
|
||||
def ohlcv_candle_limit(
|
||||
@@ -981,12 +993,12 @@ class Exchange:
|
||||
swap.linear.fetchOHLCV.limit
|
||||
"""
|
||||
feat = (
|
||||
self._api_async.features.get("spot", {})
|
||||
safe_value_nested(self._api_async.features, "spot", {})
|
||||
if market_type == "spot"
|
||||
else self._api_async.features.get("swap", {}).get("linear", {})
|
||||
else safe_value_nested(self._api_async.features, "swap.linear", {})
|
||||
)
|
||||
|
||||
return feat.get(endpoint, {}).get(attribute, default)
|
||||
return safe_value_nested(feat, f"{endpoint}.{attribute}", default)
|
||||
|
||||
def get_precision_amount(self, pair: str) -> float | None:
|
||||
"""
|
||||
@@ -1155,7 +1167,7 @@ class Exchange:
|
||||
orderbook: OrderBook | None = None
|
||||
if self.exchange_has("fetchL2OrderBook"):
|
||||
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||
if ordertype == "limit" and orderbook:
|
||||
if not stop_loss and ordertype == "limit" and orderbook:
|
||||
# Allow a 1% price difference
|
||||
allowed_diff = 0.01
|
||||
if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
|
||||
@@ -1292,6 +1304,7 @@ class Exchange:
|
||||
Check dry-run limit order fill and update fee (if it filled).
|
||||
"""
|
||||
if order["status"] != "closed" and order.get("ft_order_type") == "stoploss":
|
||||
# Stoploss branch
|
||||
pair = order["symbol"]
|
||||
if not orderbook and self.exchange_has("fetchL2OrderBook"):
|
||||
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||
@@ -1299,6 +1312,11 @@ class Exchange:
|
||||
crossed = self._dry_is_price_crossed(
|
||||
pair, order["side"], price, orderbook, is_stop=True
|
||||
)
|
||||
if crossed and immediate:
|
||||
raise InvalidOrderException(
|
||||
"Could not create dry stoploss order. Stoploss would trigger immediately."
|
||||
)
|
||||
|
||||
if crossed:
|
||||
average = self.get_dry_market_fill_price(
|
||||
pair,
|
||||
@@ -1827,16 +1845,16 @@ class Exchange:
|
||||
return order
|
||||
|
||||
@retrier
|
||||
def get_balances(self) -> CcxtBalances:
|
||||
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||
try:
|
||||
balances = self._api.fetch_balance()
|
||||
balances = self._api.fetch_balance(params or {})
|
||||
# Remove additional info from ccxt results
|
||||
balances.pop("info", None)
|
||||
balances.pop("free", None)
|
||||
balances.pop("total", None)
|
||||
balances.pop("used", None)
|
||||
|
||||
self._log_exchange_response("fetch_balance", balances)
|
||||
self._log_exchange_response("fetch_balance", balances, add_info=params)
|
||||
return balances
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
@@ -1848,7 +1866,9 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def fetch_positions(self, pair: str | None = None) -> list[CcxtPosition]:
|
||||
def fetch_positions(
|
||||
self, pair: str | None = None, params: dict | None = None
|
||||
) -> list[CcxtPosition]:
|
||||
"""
|
||||
Fetch positions from the exchange.
|
||||
If no pair is given, all positions are returned.
|
||||
@@ -1860,7 +1880,7 @@ class Exchange:
|
||||
symbols = None
|
||||
if pair:
|
||||
symbols = [pair]
|
||||
positions: list[CcxtPosition] = self._api.fetch_positions(symbols)
|
||||
positions: list[CcxtPosition] = self._api.fetch_positions(symbols, params=params or {})
|
||||
self._log_exchange_response("fetch_positions", positions)
|
||||
return positions
|
||||
except ccxt.DDoSProtection as e:
|
||||
@@ -2060,12 +2080,13 @@ class Exchange:
|
||||
"""
|
||||
return self._config["stake_currency"]
|
||||
|
||||
def get_conversion_rate(self, coin: str, currency: str) -> float | None:
|
||||
def get_conversion_rate(self, coin: str, currency: str, *, cached=True) -> float | None:
|
||||
"""
|
||||
Quick and cached way to get conversion rate one currency to the other.
|
||||
Can then be used as "rate * amount" to convert between currencies.
|
||||
:param coin: Coin to convert
|
||||
:param currency: Currency to convert to
|
||||
:param cached: Allow cached tickers, default True
|
||||
:returns: Conversion rate from coin to currency
|
||||
:raises: ExchangeErrors
|
||||
"""
|
||||
@@ -2076,13 +2097,13 @@ class Exchange:
|
||||
currency = proxy_currency
|
||||
if coin == currency:
|
||||
return 1.0
|
||||
tickers = self.get_tickers(cached=True)
|
||||
tickers = self.get_tickers(cached=cached)
|
||||
try:
|
||||
for pair in self.get_valid_pair_combination(coin, currency):
|
||||
ticker: Ticker | None = tickers.get(pair, None)
|
||||
if not ticker:
|
||||
tickers_other: Tickers = self.get_tickers(
|
||||
cached=True,
|
||||
cached=cached,
|
||||
market_type=(
|
||||
TradingMode.SPOT
|
||||
if self.trading_mode != TradingMode.SPOT
|
||||
@@ -2091,7 +2112,7 @@ class Exchange:
|
||||
)
|
||||
ticker = tickers_other.get(pair, None)
|
||||
if ticker:
|
||||
rate: float | None = safe_value_fallback2(ticker, ticker, "last", "ask", None)
|
||||
rate: float | None = safe_value_fallback(ticker, "last", "ask", None)
|
||||
if rate and pair.startswith(currency) and not pair.endswith(currency):
|
||||
rate = 1.0 / rate
|
||||
return rate
|
||||
@@ -2391,6 +2412,16 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
||||
"""
|
||||
Return order id or id_stop (for conditional orders) based on exchange settings
|
||||
|
||||
:param order: ccxt order dict
|
||||
:return: correct order id
|
||||
"""
|
||||
if self.get_option("stoploss_query_requires_stop_flag") and (
|
||||
order["type"] in ("stoploss", "stop")
|
||||
):
|
||||
return safe_value_fallback(order, "id_stop", "id")
|
||||
return order["id"]
|
||||
|
||||
@retrier
|
||||
@@ -2550,7 +2581,13 @@ class Exchange:
|
||||
)
|
||||
)
|
||||
logger.debug(f"Downloaded data for {pair} from ccxt with length {len(data)}.")
|
||||
return ohlcv_to_dataframe(data, timeframe, pair, fill_missing=False, drop_incomplete=True)
|
||||
# funding_rates are always complete, so never need to be dropped.
|
||||
drop_incomplete = (
|
||||
self._ohlcv_partial_candle if candle_type != CandleType.FUNDING_RATE else False
|
||||
)
|
||||
return ohlcv_to_dataframe(
|
||||
data, timeframe, pair, fill_missing=False, drop_incomplete=drop_incomplete
|
||||
)
|
||||
|
||||
async def _async_get_historic_ohlcv(
|
||||
self,
|
||||
@@ -2876,8 +2913,11 @@ class Exchange:
|
||||
}
|
||||
pairs_to_download = [p for p in pairs if p not in candles]
|
||||
if pairs_to_download:
|
||||
candles = self.refresh_latest_ohlcv(pairs_to_download, since_ms=since_ms, cache=False)
|
||||
for c, val in candles.items():
|
||||
candles_new = self.refresh_latest_ohlcv(
|
||||
pairs_to_download, since_ms=since_ms, cache=False
|
||||
)
|
||||
for c, val in candles_new.items():
|
||||
candles[c] = val
|
||||
self._expiring_candle_cache[(c[1], since_ms)][c] = val
|
||||
return candles
|
||||
|
||||
@@ -3602,7 +3642,7 @@ class Exchange:
|
||||
pair_tiers.append(self.parse_leverage_tier(tier))
|
||||
self._leverage_tiers[pair] = pair_tiers
|
||||
|
||||
def parse_leverage_tier(self, tier) -> dict:
|
||||
def parse_leverage_tier(self, tier) -> LeverageTier:
|
||||
info = tier.get("info", {})
|
||||
return {
|
||||
"minNotional": tier["minNotional"],
|
||||
@@ -3643,7 +3683,11 @@ class Exchange:
|
||||
for tier in pair_tiers:
|
||||
# Adjust notional by leverage to do a proper comparison
|
||||
min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"])
|
||||
max_stake = tier["maxNotional"] / tier["maxLeverage"]
|
||||
max_stake = (
|
||||
tier["maxNotional"] / tier["maxLeverage"]
|
||||
if tier["maxNotional"] is not None
|
||||
else float("inf")
|
||||
)
|
||||
prior_max_lev = tier["maxLeverage"]
|
||||
if min_stake <= stake_amount <= max_stake:
|
||||
return tier["maxLeverage"]
|
||||
|
||||
@@ -115,5 +115,27 @@ class CcxtPosition(TypedDict):
|
||||
|
||||
CcxtOrder = dict[str, Any]
|
||||
|
||||
|
||||
class LeverageTier(TypedDict):
|
||||
"""
|
||||
Represents a single leverage tier returned by the exchange.
|
||||
|
||||
Attributes:
|
||||
minNotional: Minimum notional value (quote currency) for which this tier applies.
|
||||
maxNotional: Maximum notional value (quote currency) for which this tier applies.
|
||||
When ``maxNotional`` is ``None``, the tier is unbounded on the upper side,
|
||||
i.e. there is no maximum notional limit for this tier
|
||||
maintenanceMarginRate: Maintenance margin rate for this tier (fraction, e.g. 0.005 for 0.5%)
|
||||
maxLeverage: Maximum leverage allowed for this tier
|
||||
maintAmt: Optional fixed maintenance margin amount, if provided by the exchange
|
||||
"""
|
||||
|
||||
minNotional: float
|
||||
maxNotional: float | None
|
||||
maintenanceMarginRate: float
|
||||
maxLeverage: float
|
||||
maintAmt: float | None
|
||||
|
||||
|
||||
# pair, timeframe, candleType, OHLCV, drop last?,
|
||||
OHLCVResponse = tuple[str, str, CandleType, list, bool]
|
||||
|
||||
@@ -22,6 +22,7 @@ from ccxt import (
|
||||
from freqtrade.exchange.common import (
|
||||
BAD_EXCHANGES,
|
||||
EXCHANGE_HAS_OPTIONAL,
|
||||
EXCHANGE_HAS_OPTIONAL_FUTURES,
|
||||
EXCHANGE_HAS_REQUIRED,
|
||||
MAP_EXCHANGE_CHILDCLASS,
|
||||
SUPPORTED_EXCHANGES,
|
||||
@@ -53,7 +54,22 @@ def available_exchanges(ccxt_module: CcxtModuleType | None = None) -> list[str]:
|
||||
return [x for x in exchanges if validate_exchange(x)[0]]
|
||||
|
||||
|
||||
def validate_exchange(exchange: str) -> tuple[bool, str, ccxt.Exchange | None]:
|
||||
def _exchange_has_helper(ex_mod: ccxt.Exchange, required: dict[str, list[str]]) -> list[str]:
|
||||
"""
|
||||
Checks availability of methods (or their replacement)s in ex_mod.has
|
||||
:param ex_mod: ccxt Exchange module
|
||||
:param required: dict of required methods, with possible replacement methods as list
|
||||
:return: list of missing required methods
|
||||
"""
|
||||
return [
|
||||
k
|
||||
for k, v in required.items()
|
||||
if ex_mod.has.get(k) is not True
|
||||
and (len(v) == 0 or not (all(ex_mod.has.get(x) for x in v)))
|
||||
]
|
||||
|
||||
|
||||
def validate_exchange(exchange: str) -> tuple[bool, str, str, ccxt.Exchange | None]:
|
||||
"""
|
||||
returns: can_use, reason, exchange_object
|
||||
with Reason including both missing and missing_opt
|
||||
@@ -64,36 +80,38 @@ def validate_exchange(exchange: str) -> tuple[bool, str, ccxt.Exchange | None]:
|
||||
ex_mod = getattr(ccxt.async_support, exchange.lower())()
|
||||
|
||||
if not ex_mod or not ex_mod.has:
|
||||
return False, "", None
|
||||
return False, "", "", None
|
||||
|
||||
result = True
|
||||
reason = ""
|
||||
missing = [
|
||||
k
|
||||
for k, v in EXCHANGE_HAS_REQUIRED.items()
|
||||
if ex_mod.has.get(k) is not True and not (all(ex_mod.has.get(x) for x in v))
|
||||
]
|
||||
reasons = []
|
||||
reasons_fut = ""
|
||||
missing = _exchange_has_helper(ex_mod, EXCHANGE_HAS_REQUIRED)
|
||||
if missing:
|
||||
result = False
|
||||
reason += f"missing: {', '.join(missing)}"
|
||||
reasons.append(f"missing: {', '.join(missing)}")
|
||||
|
||||
missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)]
|
||||
missing_opt = _exchange_has_helper(ex_mod, EXCHANGE_HAS_OPTIONAL)
|
||||
|
||||
missing_futures = _exchange_has_helper(ex_mod, EXCHANGE_HAS_OPTIONAL_FUTURES)
|
||||
|
||||
if exchange.lower() in BAD_EXCHANGES:
|
||||
result = False
|
||||
reason = BAD_EXCHANGES.get(exchange.lower(), "")
|
||||
reasons.append(BAD_EXCHANGES.get(exchange.lower(), ""))
|
||||
|
||||
if missing_opt:
|
||||
reason += f"{'. ' if reason else ''}missing opt: {', '.join(missing_opt)}. "
|
||||
reasons.append(f"missing opt: {', '.join(missing_opt)}")
|
||||
|
||||
return result, reason, ex_mod
|
||||
if missing_futures:
|
||||
reasons_fut = f"missing futures opt: {', '.join(missing_futures)}"
|
||||
|
||||
return result, "; ".join(reasons), reasons_fut, ex_mod
|
||||
|
||||
|
||||
def _build_exchange_list_entry(
|
||||
exchange_name: str, exchangeClasses: dict[str, Any]
|
||||
) -> ValidExchangesType:
|
||||
exchange_name = exchange_name.lower()
|
||||
valid, comment, ex_mod = validate_exchange(exchange_name)
|
||||
valid, comment, comment_fut, ex_mod = validate_exchange(exchange_name)
|
||||
mapped_exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name).lower()
|
||||
is_alias = getattr(ex_mod, "alias", False)
|
||||
result: ValidExchangesType = {
|
||||
@@ -102,6 +120,7 @@ def _build_exchange_list_entry(
|
||||
"valid": valid,
|
||||
"supported": mapped_exchange_name in SUPPORTED_EXCHANGES and not is_alias,
|
||||
"comment": comment,
|
||||
"comment_futures": comment_fut,
|
||||
"dex": getattr(ex_mod, "dex", False),
|
||||
"is_alias": is_alias,
|
||||
"alias_for": inspect.getmro(ex_mod.__class__)[1]().id
|
||||
|
||||
@@ -10,8 +10,7 @@ from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||
from freqtrade.misc import safe_value_fallback2
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -132,6 +131,3 @@ class Gate(Exchange):
|
||||
"rate": pair_fees[takerOrMaker],
|
||||
}
|
||||
return trades
|
||||
|
||||
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
||||
return safe_value_fallback2(order, order, "id_stop", "id")
|
||||
|
||||
@@ -7,9 +7,10 @@ from typing import Any
|
||||
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.enums import MarginMode, TradingMode
|
||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.enums.runmode import NON_UTIL_MODES
|
||||
from freqtrade.exceptions import ConfigurationError, ExchangeError, OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||
from freqtrade.exchange.exchange_types import CcxtBalances, CcxtOrder, CcxtPosition, FtHas
|
||||
from freqtrade.util.datetime_helpers import dt_from_ts
|
||||
|
||||
|
||||
@@ -57,12 +58,108 @@ class Hyperliquid(Exchange):
|
||||
config.update(super()._ccxt_config)
|
||||
return config
|
||||
|
||||
def _get_configured_hip3_dexes(self) -> list[str]:
|
||||
"""Get list of configured HIP-3 DEXes."""
|
||||
return self._config.get("exchange", {}).get("hip3_dexes", [])
|
||||
|
||||
def validate_config(self, config: dict) -> None:
|
||||
"""Validate HIP-3 configuration at bot startup."""
|
||||
super().validate_config(config)
|
||||
configured = self._get_configured_hip3_dexes()
|
||||
if not configured or not self.markets:
|
||||
return
|
||||
if self.trading_mode != TradingMode.FUTURES:
|
||||
if configured:
|
||||
raise ConfigurationError(
|
||||
"HIP-3 DEXes are only supported in FUTURES trading mode. "
|
||||
"Please update your configuration!"
|
||||
)
|
||||
return
|
||||
if configured and self.margin_mode != MarginMode.ISOLATED:
|
||||
raise ConfigurationError(
|
||||
"HIP-3 DEXes require 'isolated' margin mode. "
|
||||
f"Current margin mode: '{self.margin_mode.value}'. "
|
||||
"Please update your configuration!"
|
||||
)
|
||||
|
||||
available = {
|
||||
m.get("info", {}).get("dex")
|
||||
for m in self.get_markets(
|
||||
quote_currencies=[self._config["stake_currency"]],
|
||||
tradable_only=True,
|
||||
active_only=True,
|
||||
).values()
|
||||
if m.get("info", {}).get("hip3")
|
||||
}
|
||||
available.discard(None)
|
||||
|
||||
invalid = set(configured) - available
|
||||
if invalid:
|
||||
raise ConfigurationError(
|
||||
f"Invalid HIP-3 DEXes configured: {sorted(invalid)}. "
|
||||
f"Available DEXes matching your stake currency ({self._config['stake_currency']}): "
|
||||
f"{sorted(available)}. "
|
||||
f"Check your 'hip3_dexes' configuration!"
|
||||
)
|
||||
|
||||
def market_is_tradable(self, market: dict[str, Any]) -> bool:
|
||||
"""Check if market is tradable, including HIP-3 markets."""
|
||||
parent_check = super().market_is_tradable(market)
|
||||
|
||||
# Exclude hip3 markets for now - which have the format XYZ:GOOGL/USDT:USDT -
|
||||
# and XYZ:GOOGL as base
|
||||
return parent_check and ":" not in market["base"]
|
||||
market_info = market.get("info", {})
|
||||
if market_info.get("hip3") and self._config["runmode"] in NON_UTIL_MODES:
|
||||
configured = self._get_configured_hip3_dexes()
|
||||
if not configured:
|
||||
return False
|
||||
|
||||
market_dex = market_info.get("dex")
|
||||
return parent_check and market_dex in configured
|
||||
|
||||
return parent_check
|
||||
|
||||
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||
"""Fetch balances from default DEX and HIP-3 DEXes needed by tradable pairs.
|
||||
This override is not absolutely necessary and is only there for correct used / total values
|
||||
which are however not used by Freqtrade in futures mode at the moment.
|
||||
"""
|
||||
balances = super().get_balances()
|
||||
dexes = self._get_configured_hip3_dexes()
|
||||
for dex in dexes:
|
||||
try:
|
||||
dex_balance = super().get_balances(params={"dex": dex})
|
||||
|
||||
for currency, amount_info in dex_balance.items():
|
||||
if currency in ["info", "free", "used", "total", "datetime", "timestamp"]:
|
||||
continue
|
||||
|
||||
if currency not in balances:
|
||||
balances[currency] = amount_info
|
||||
else:
|
||||
balances[currency]["free"] += amount_info["free"]
|
||||
balances[currency]["used"] += amount_info["used"]
|
||||
balances[currency]["total"] += amount_info["total"]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Could not fetch balance for HIP-3 DEX '{dex}': {e}")
|
||||
|
||||
if dexes:
|
||||
self._log_exchange_response("fetch_balance", balances, add_info="combined")
|
||||
return balances
|
||||
|
||||
def fetch_positions(
|
||||
self, pair: str | None = None, params: dict | None = None
|
||||
) -> list[CcxtPosition]:
|
||||
"""Fetch positions from default DEX and HIP-3 DEXes needed by tradable pairs."""
|
||||
positions = super().fetch_positions(pair)
|
||||
dexes = self._get_configured_hip3_dexes()
|
||||
for dex in dexes:
|
||||
try:
|
||||
positions.extend(super().fetch_positions(pair, params={"dex": dex}))
|
||||
except Exception as e:
|
||||
logger.error(f"Could not fetch positions from HIP-3 DEX '{dex}': {e}")
|
||||
if dexes:
|
||||
self._log_exchange_response("fetch_positions", positions, add_info="combined")
|
||||
return positions
|
||||
|
||||
def get_max_leverage(self, pair: str, stake_amount: float | None) -> float:
|
||||
# There are no leverage tiers
|
||||
|
||||
@@ -70,7 +70,7 @@ class Kraken(Exchange):
|
||||
return consolidated
|
||||
|
||||
@retrier
|
||||
def get_balances(self) -> CcxtBalances:
|
||||
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||
if self._config["dry_run"]:
|
||||
return {}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from freqtrade.exceptions import (
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||
from freqtrade.misc import safe_value_fallback2
|
||||
from freqtrade.util import dt_now, dt_ts
|
||||
|
||||
|
||||
@@ -183,7 +182,10 @@ class Okx(Exchange):
|
||||
return float("inf")
|
||||
|
||||
pair_tiers = self._leverage_tiers[pair]
|
||||
return pair_tiers[-1]["maxNotional"] / leverage
|
||||
last_max_notional = pair_tiers[-1]["maxNotional"]
|
||||
if last_max_notional is None:
|
||||
return float("inf")
|
||||
return last_max_notional / leverage
|
||||
|
||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> dict:
|
||||
params = super()._get_stop_params(side, ordertype, stop_price)
|
||||
@@ -259,11 +261,6 @@ class Okx(Exchange):
|
||||
raise OperationalException(e) from e
|
||||
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
|
||||
|
||||
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
||||
if order.get("type", "") == "stop":
|
||||
return safe_value_fallback2(order, order, "id_stop", "id")
|
||||
return order["id"]
|
||||
|
||||
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
|
||||
orders = []
|
||||
|
||||
|
||||
@@ -446,7 +446,7 @@ class FreqaiDataDrawer:
|
||||
|
||||
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
|
||||
|
||||
pattern = re.compile(r"sub-train-(\w+)_(\d{10})")
|
||||
pattern = re.compile(r"^sub-train-(.+)_(\d{10})$")
|
||||
|
||||
delete_dict: dict[str, Any] = {}
|
||||
|
||||
|
||||
@@ -227,6 +227,10 @@ class IFreqaiModel(ABC):
|
||||
"""
|
||||
while not self._stop_event.is_set():
|
||||
time.sleep(1)
|
||||
|
||||
if not self.train_queue:
|
||||
continue
|
||||
|
||||
pair = self.train_queue[0]
|
||||
|
||||
# ensure pair is available in dp
|
||||
|
||||
@@ -2421,7 +2421,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
def handle_protections(self, pair: str, side: LongShort) -> None:
|
||||
# Lock pair for one candle to prevent immediate re-entries
|
||||
self.strategy.lock_pair(pair, datetime.now(UTC), reason="Auto lock", side=side)
|
||||
prot_trig = self.protections.stop_per_pair(pair, side=side)
|
||||
starting_balance = self.wallets.get_starting_balance()
|
||||
prot_trig = self.protections.stop_per_pair(
|
||||
pair, side=side, starting_balance=starting_balance
|
||||
)
|
||||
if prot_trig:
|
||||
msg: RPCProtectionMsg = {
|
||||
"type": RPCMessageType.PROTECTION_TRIGGER,
|
||||
@@ -2430,7 +2433,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
}
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
prot_trig_glb = self.protections.global_stop(side=side)
|
||||
prot_trig_glb = self.protections.global_stop(side=side, starting_balance=starting_balance)
|
||||
if prot_trig_glb:
|
||||
msg = {
|
||||
"type": RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
|
||||
|
||||
@@ -6,25 +6,37 @@ from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class _BaseAnnotationType(TypedDict, total=False):
|
||||
start: str | datetime
|
||||
end: str | datetime
|
||||
y_start: float
|
||||
y_end: float
|
||||
color: str
|
||||
label: str
|
||||
z_level: int
|
||||
|
||||
|
||||
class AreaAnnotationType(_BaseAnnotationType, total=False):
|
||||
class _Base2DAnnotationType(_BaseAnnotationType, total=False):
|
||||
start: str | datetime
|
||||
end: str | datetime
|
||||
y_start: float
|
||||
y_end: float
|
||||
|
||||
|
||||
class AreaAnnotationType(_Base2DAnnotationType, total=False):
|
||||
type: Required[Literal["area"]]
|
||||
|
||||
|
||||
class LineAnnotationType(_BaseAnnotationType, total=False):
|
||||
class LineAnnotationType(_Base2DAnnotationType, total=False):
|
||||
type: Required[Literal["line"]]
|
||||
width: int
|
||||
line_style: Literal["solid", "dashed", "dotted"]
|
||||
|
||||
|
||||
AnnotationType = AreaAnnotationType | LineAnnotationType
|
||||
class PointAnnotationType(_BaseAnnotationType, total=False):
|
||||
type: Required[Literal["point"]]
|
||||
x: str | datetime
|
||||
y: float
|
||||
size: int
|
||||
shape: Literal["circle", "rect", "roundRect", "triangle", "pin", "arrow", "none"]
|
||||
rotate: int
|
||||
|
||||
|
||||
AnnotationType = AreaAnnotationType | LineAnnotationType | PointAnnotationType
|
||||
|
||||
AnnotationTypeTA: TypeAdapter[AnnotationType] = TypeAdapter(AnnotationType)
|
||||
|
||||
@@ -14,6 +14,7 @@ class ValidExchangesType(TypedDict):
|
||||
valid: bool
|
||||
supported: bool
|
||||
comment: str
|
||||
comment_futures: str
|
||||
dex: bool
|
||||
is_alias: bool
|
||||
alias_for: str | None
|
||||
|
||||
+33
-7
@@ -84,7 +84,12 @@ def file_load_json(file: Path):
|
||||
|
||||
def is_file_in_dir(file: Path, directory: Path) -> bool:
|
||||
"""
|
||||
Helper function to check if file is in directory.
|
||||
Helper function to check if file is directly within a directory.
|
||||
:param file: File to check
|
||||
:param directory: Directory to check against
|
||||
When used in the API, this parameter cannot be user controlled (outside of the config)
|
||||
to avoid security issues.
|
||||
:return: True if file is directly within directory, False otherwise
|
||||
"""
|
||||
return file.is_file() and file.parent.samefile(directory)
|
||||
|
||||
@@ -125,6 +130,27 @@ def round_dict(d, n):
|
||||
DictMap = dict[str, Any] | Mapping[str, Any]
|
||||
|
||||
|
||||
def safe_value_nested(obj: DictMap, keys: str, default_value=None):
|
||||
"""
|
||||
Search a nested dict for a value.
|
||||
:param obj: dict to search in
|
||||
:param keys: dot separated keys to search for
|
||||
:param default_value: value to return if the key is not found or value is None
|
||||
:return: value found in dict or default_value
|
||||
Sample:
|
||||
>>> d = { 'first' : { 'rows' : { 'pass' : 'dog', 'number' : '1' } } }
|
||||
>>> safe_value_nested(d, "first.rows.pass") == "dog"
|
||||
True
|
||||
"""
|
||||
nested_obj = obj
|
||||
for key in keys.split("."):
|
||||
if isinstance(nested_obj, Mapping) and key in nested_obj and nested_obj[key] is not None:
|
||||
nested_obj = nested_obj[key]
|
||||
else:
|
||||
return default_value
|
||||
return nested_obj
|
||||
|
||||
|
||||
def safe_value_fallback(obj: DictMap, key1: str, key2: str | None = None, default_value=None):
|
||||
"""
|
||||
Search a value in obj, return this if it's not None.
|
||||
@@ -210,12 +236,12 @@ def remove_entry_exit_signals(dataframe: pd.DataFrame):
|
||||
|
||||
:param dataframe: The DataFrame to remove signals from
|
||||
"""
|
||||
dataframe[SignalType.ENTER_LONG.value] = 0
|
||||
dataframe[SignalType.EXIT_LONG.value] = 0
|
||||
dataframe[SignalType.ENTER_SHORT.value] = 0
|
||||
dataframe[SignalType.EXIT_SHORT.value] = 0
|
||||
dataframe[SignalTagType.ENTER_TAG.value] = None
|
||||
dataframe[SignalTagType.EXIT_TAG.value] = None
|
||||
dataframe[SignalType.ENTER_LONG] = 0
|
||||
dataframe[SignalType.EXIT_LONG] = 0
|
||||
dataframe[SignalType.ENTER_SHORT] = 0
|
||||
dataframe[SignalType.EXIT_SHORT] = 0
|
||||
dataframe[SignalTagType.ENTER_TAG] = None
|
||||
dataframe[SignalTagType.EXIT_TAG] = None
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ class Backtesting:
|
||||
"exited": {},
|
||||
}
|
||||
self.rejected_dict: dict[str, list] = {}
|
||||
self.starting_balance: float = 0.0
|
||||
|
||||
self._exchange_name = self.config["exchange"]["name"]
|
||||
self.__initial_backtest = exchange is None
|
||||
@@ -277,6 +278,7 @@ class Backtesting:
|
||||
self.reset_backtest(False)
|
||||
|
||||
self.wallets = Wallets(self.config, self.exchange, is_backtest=True)
|
||||
self.starting_balance = self.wallets.get_starting_balance()
|
||||
|
||||
self.progress = BTProgress()
|
||||
self.abort = False
|
||||
@@ -439,6 +441,8 @@ class Backtesting:
|
||||
PairLocks.reset_locks()
|
||||
Trade.reset_trades()
|
||||
CustomDataWrapper.reset_custom_data()
|
||||
# Ensure logging is disabled in other processes during hyperopt
|
||||
LoggingMixin.show_output = False
|
||||
self.rejected_trades = 0
|
||||
self.timedout_entry_orders = 0
|
||||
self.timedout_exit_orders = 0
|
||||
@@ -603,8 +607,6 @@ class Backtesting:
|
||||
trade_dur: int,
|
||||
) -> float:
|
||||
is_short = trade.is_short or False
|
||||
leverage = trade.leverage or 1.0
|
||||
side_1 = -1 if is_short else 1
|
||||
roi_entry, roi = self.strategy.min_roi_reached_entry(
|
||||
trade, # type: ignore[arg-type]
|
||||
trade_dur,
|
||||
@@ -617,10 +619,7 @@ class Backtesting:
|
||||
# - we'll use open instead of close
|
||||
return row[OPEN_IDX]
|
||||
|
||||
# - (Expected abs profit - open_rate - open_fee) / (fee_close -1)
|
||||
roi_rate = trade.open_rate * roi / leverage
|
||||
open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open)
|
||||
close_rate = -(roi_rate + open_fee_rate) / ((trade.fee_close or 0.0) - side_1 * 1)
|
||||
close_rate = trade.calc_close_rate_for_roi(roi)
|
||||
if is_short:
|
||||
is_new_roi = row[OPEN_IDX] < close_rate
|
||||
else:
|
||||
@@ -1274,8 +1273,8 @@ class Backtesting:
|
||||
|
||||
def run_protections(self, pair: str, current_time: datetime, side: LongShort):
|
||||
if self.enable_protections:
|
||||
self.protections.stop_per_pair(pair, current_time, side)
|
||||
self.protections.global_stop(current_time, side)
|
||||
self.protections.stop_per_pair(pair, current_time, side, self.starting_balance)
|
||||
self.protections.global_stop(current_time, side, self.starting_balance)
|
||||
|
||||
def manage_open_orders(self, trade: LocalTrade, current_time: datetime, row: tuple) -> bool:
|
||||
"""
|
||||
|
||||
@@ -21,6 +21,11 @@ def logging_mp_setup(log_queue: Queue, verbosity: int):
|
||||
root = logging.getLogger()
|
||||
root.setLevel(verbosity)
|
||||
root.addHandler(h)
|
||||
# Disable freqtrade logging outside of the main process
|
||||
# This only leaves logging from the strategy (unless it's prefixed with "freqtrade.")
|
||||
# and eventually from other libraries.
|
||||
if verbosity > logging.DEBUG:
|
||||
logging.getLogger("freqtrade").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def logging_mp_handle(q: Queue):
|
||||
|
||||
@@ -6,7 +6,7 @@ and will be sent to the hyperopt worker processes.
|
||||
import logging
|
||||
import sys
|
||||
import warnings
|
||||
from datetime import UTC, datetime
|
||||
from datetime import datetime
|
||||
from multiprocessing import Manager
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -42,6 +42,7 @@ from freqtrade.optimize.space import (
|
||||
ft_IntDistribution,
|
||||
)
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||
from freqtrade.util import dt_now
|
||||
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
|
||||
|
||||
|
||||
@@ -269,7 +270,7 @@ class HyperOptimizer:
|
||||
Keep this function as optimized as possible!
|
||||
"""
|
||||
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
|
||||
backtest_start_time = datetime.now(UTC)
|
||||
backtest_start_time = dt_now()
|
||||
|
||||
for attr_name, attr in self.backtesting.strategy.enumerate_parameters():
|
||||
if attr.in_space and attr.optimize:
|
||||
@@ -320,7 +321,7 @@ class HyperOptimizer:
|
||||
bt_results = self.backtesting.backtest(
|
||||
processed=processed, start_date=self.min_date, end_date=self.max_date
|
||||
)
|
||||
backtest_end_time = datetime.now(UTC)
|
||||
backtest_end_time = dt_now()
|
||||
bt_results.update(
|
||||
{
|
||||
"backtest_start_time": int(backtest_start_time.timestamp()),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
from typing import ClassVar, Literal
|
||||
|
||||
from sqlalchemy import String
|
||||
@@ -11,7 +11,7 @@ from freqtrade.persistence.base import ModelBase, SessionType
|
||||
ValueTypes = str | datetime | float | int
|
||||
|
||||
|
||||
class ValueTypesEnum(str, Enum):
|
||||
class ValueTypesEnum(StrEnum):
|
||||
STRING = "str"
|
||||
DATETIME = "datetime"
|
||||
FLOAT = "float"
|
||||
|
||||
@@ -30,25 +30,39 @@ def get_backup_name(tabs: list[str], backup_prefix: str):
|
||||
return table_back_name
|
||||
|
||||
|
||||
def get_last_sequence_ids(engine, trade_back_name: str, order_back_name: str):
|
||||
order_id: int | None = None
|
||||
trade_id: int | None = None
|
||||
def get_last_sequence_ids(engine, sequence_name: str, table_back_name: str) -> int | None:
|
||||
last_id: int | None = None
|
||||
|
||||
if engine.name == "postgresql":
|
||||
with engine.begin() as connection:
|
||||
trade_id = connection.execute(text("select nextval('trades_id_seq')")).fetchone()[0]
|
||||
order_id = connection.execute(text("select nextval('orders_id_seq')")).fetchone()[0]
|
||||
last_id = connection.execute(text(f"select nextval('{sequence_name}')")).fetchone()[0]
|
||||
with engine.begin() as connection:
|
||||
connection.execute(
|
||||
text(f"ALTER SEQUENCE orders_id_seq rename to {order_back_name}_id_seq_bak")
|
||||
text(f"ALTER SEQUENCE {sequence_name} rename to {table_back_name}_id_seq_bak")
|
||||
)
|
||||
connection.execute(
|
||||
text(f"ALTER SEQUENCE trades_id_seq rename to {trade_back_name}_id_seq_bak")
|
||||
)
|
||||
return order_id, trade_id
|
||||
|
||||
return last_id
|
||||
|
||||
|
||||
def set_sequence_ids(engine, order_id, trade_id, pairlock_id=None):
|
||||
def set_sequence_ids(
|
||||
engine,
|
||||
order_id: int | None = None,
|
||||
trade_id: int | None = None,
|
||||
pairlock_id: int | None = None,
|
||||
kv_id: int | None = None,
|
||||
custom_data_id: int | None = None,
|
||||
):
|
||||
"""
|
||||
Set sequence ids to the given values.
|
||||
The id's given should be the next id to use, so the current max id + 1 - or current id
|
||||
if using nextval before migration.
|
||||
:param engine: SQLAlchemy engine
|
||||
:param order_id: value to set for orders_id_seq (optional)
|
||||
:param trade_id: value to set for trades_id_seq (optional)
|
||||
:param pairlock_id: value to set for pairlocks_id_seq (optional)
|
||||
:param kv_id: value to set for KeyValueStore_id_seq (optional)
|
||||
:param custom_data_id: value to set for trade_custom_data_id_seq (optional)
|
||||
"""
|
||||
if engine.name == "postgresql":
|
||||
with engine.begin() as connection:
|
||||
if order_id:
|
||||
@@ -59,6 +73,14 @@ def set_sequence_ids(engine, order_id, trade_id, pairlock_id=None):
|
||||
connection.execute(
|
||||
text(f"ALTER SEQUENCE pairlocks_id_seq RESTART WITH {pairlock_id}")
|
||||
)
|
||||
if kv_id:
|
||||
connection.execute(
|
||||
text(f'ALTER SEQUENCE "KeyValueStore_id_seq" RESTART WITH {kv_id}')
|
||||
)
|
||||
if custom_data_id:
|
||||
connection.execute(
|
||||
text(f"ALTER SEQUENCE trade_custom_data_id_seq RESTART WITH {custom_data_id}")
|
||||
)
|
||||
|
||||
|
||||
def drop_index_on_table(engine, inspector, table_bak_name):
|
||||
@@ -157,7 +179,8 @@ def migrate_trades_and_orders_table(
|
||||
|
||||
drop_index_on_table(engine, inspector, trade_back_name)
|
||||
|
||||
order_id, trade_id = get_last_sequence_ids(engine, trade_back_name, order_back_name)
|
||||
order_id = get_last_sequence_ids(engine, "order_id_seq", order_back_name)
|
||||
trade_id = get_last_sequence_ids(engine, "trades_id_seq", trade_back_name)
|
||||
|
||||
drop_orders_table(engine, order_back_name)
|
||||
|
||||
@@ -269,6 +292,7 @@ def migrate_pairlocks_table(decl_base, inspector, engine, pairlock_back_name: st
|
||||
connection.execute(text(f"alter table pairlocks rename to {pairlock_back_name}"))
|
||||
|
||||
drop_index_on_table(engine, inspector, pairlock_back_name)
|
||||
pairlock_id = get_last_sequence_ids(engine, "pairlocks_id_seq", pairlock_back_name)
|
||||
|
||||
side = get_column_def(cols, "side", "'*'")
|
||||
|
||||
@@ -288,6 +312,8 @@ def migrate_pairlocks_table(decl_base, inspector, engine, pairlock_back_name: st
|
||||
)
|
||||
)
|
||||
|
||||
set_sequence_ids(engine, pairlock_id=pairlock_id)
|
||||
|
||||
|
||||
def set_sqlite_to_wal(engine):
|
||||
if engine.name == "sqlite" and str(engine.url) != "sqlite://":
|
||||
|
||||
@@ -86,7 +86,7 @@ class PairLocks:
|
||||
lock
|
||||
for lock in PairLocks.locks
|
||||
if (
|
||||
lock.lock_end_time >= now
|
||||
lock.lock_end_time > now
|
||||
and lock.active is True
|
||||
and (pair is None or lock.pair == pair)
|
||||
and (side is None or lock.side == "*" or lock.side == side)
|
||||
|
||||
@@ -1208,6 +1208,35 @@ class LocalTrade:
|
||||
|
||||
return float(f"{profit_ratio:.8f}")
|
||||
|
||||
def calc_close_rate_for_roi(self, target_roi: float) -> float:
|
||||
"""
|
||||
Calculate the required close price to reach a target ROI.
|
||||
Must match the logic used in `calc_profit_ratio()`.
|
||||
|
||||
:param target_roi: The desired return on investment (as a decimal, e.g., 0.05 for 5%)
|
||||
:return: Close price (rate) required to achieve the target ROI
|
||||
"""
|
||||
leverage = float(self.leverage or 1.0)
|
||||
deleveraged_roi = float(target_roi) / leverage
|
||||
|
||||
open_value = self._calc_open_trade_value(self.amount, self.open_rate)
|
||||
|
||||
# The ROI formula uses close_value(rate), which depends on trading mode:
|
||||
# - SPOT: linear in rate, adjusted by close fee
|
||||
# - MARGIN: same, but long subtracts interest, short increases amount
|
||||
# - FUTURES: adds/subtracts funding to/from close value
|
||||
# All cases are affine in rate:
|
||||
# close_value(rate) = a * rate + b
|
||||
# We extract a and b by probing close_value at rate = 0 and 1.
|
||||
value_at_0 = self.calc_close_trade_value(0.0)
|
||||
value_at_1 = self.calc_close_trade_value(1.0)
|
||||
alpha = value_at_1 - value_at_0
|
||||
beta = value_at_0
|
||||
|
||||
s = -1.0 if self.is_short else 1.0
|
||||
adj = 1.0 + (deleveraged_roi / s)
|
||||
return (adj * open_value - beta) / alpha
|
||||
|
||||
def recalc_trade_from_orders(self, *, is_closing: bool = False):
|
||||
ZERO = FtPrecise(0.0)
|
||||
current_amount = FtPrecise(0.0)
|
||||
|
||||
@@ -261,10 +261,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
||||
if trades is not None and len(trades) > 0:
|
||||
# Create description for exit summarizing the trade
|
||||
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['exit_reason']}, "
|
||||
+ f"{row['trade_duration']} min",
|
||||
lambda row: (
|
||||
f"{row['profit_ratio']:.2%}, "
|
||||
+ (f"{row['enter_tag']}, " if row["enter_tag"] is not None else "")
|
||||
+ f"{row['exit_reason']}, "
|
||||
+ f"{row['trade_duration']} min"
|
||||
),
|
||||
axis=1,
|
||||
)
|
||||
trade_entries = go.Scatter(
|
||||
|
||||
@@ -5,7 +5,7 @@ PairList Handler base class
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
from typing import Any, Literal, TypedDict
|
||||
|
||||
from freqtrade.constants import Config
|
||||
@@ -58,7 +58,7 @@ PairlistParameter = (
|
||||
)
|
||||
|
||||
|
||||
class SupportsBacktesting(str, Enum):
|
||||
class SupportsBacktesting(StrEnum):
|
||||
"""
|
||||
Enum to indicate if a Pairlist Handler supports backtesting.
|
||||
"""
|
||||
|
||||
@@ -7,6 +7,7 @@ Provides dynamic pair list based on Market Cap
|
||||
import logging
|
||||
import math
|
||||
|
||||
from freqtrade.constants import PairPrefixes
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.exchange_types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
@@ -162,9 +163,6 @@ class MarketCapPairList(IPairList):
|
||||
|
||||
return pairlist
|
||||
|
||||
# Prefixes to test to discover coins like 1000PEPE/USDDT:USDT or KPEPE/USDC (hyperliquid)
|
||||
prefixes = ("1000", "K")
|
||||
|
||||
def resolve_marketcap_pair(
|
||||
self,
|
||||
pair: str,
|
||||
@@ -179,7 +177,7 @@ class MarketCapPairList(IPairList):
|
||||
return pair
|
||||
|
||||
if pair not in markets:
|
||||
for prefix in self.prefixes:
|
||||
for prefix in PairPrefixes:
|
||||
test_prefix = f"{prefix}{pair}"
|
||||
|
||||
if test_prefix in pairlist:
|
||||
|
||||
@@ -53,7 +53,7 @@ class PercentChangePairList(IPairList):
|
||||
self._sort_direction: str | None = self._pairlistconfig.get("sort_direction", "desc")
|
||||
self._def_candletype = self._config["candle_type_def"]
|
||||
|
||||
if (self._lookback_days > 0) & (self._lookback_period > 0):
|
||||
if (self._lookback_days > 0) and (self._lookback_period > 0):
|
||||
raise OperationalException(
|
||||
"Ambiguous configuration: lookback_days and lookback_period both set in pairlist "
|
||||
"config. Please set lookback_days only or lookback_period and lookback_timeframe "
|
||||
@@ -70,7 +70,7 @@ class PercentChangePairList(IPairList):
|
||||
_tf_in_sec = self._tf_in_min * 60
|
||||
|
||||
# whether to use range lookback or not
|
||||
self._use_range = (self._tf_in_min > 0) & (self._lookback_period > 0)
|
||||
self._use_range = (self._tf_in_min > 0) and (self._lookback_period > 0)
|
||||
|
||||
if self._use_range & (self._refresh_period < _tf_in_sec):
|
||||
raise OperationalException(
|
||||
@@ -84,9 +84,9 @@ class PercentChangePairList(IPairList):
|
||||
and self._exchange.get_option("tickers_have_percentage")
|
||||
):
|
||||
raise OperationalException(
|
||||
"Exchange does not support dynamic whitelist in this configuration. "
|
||||
"Please edit your config and either remove PercentChangePairList, "
|
||||
"or switch to using candles. and restart the bot."
|
||||
f"Exchange {self._exchange.name} does not support dynamic whitelist in this "
|
||||
"configuration. Please edit your config and either remove PercentChangePairList, "
|
||||
"or switch to using candles and restart the bot."
|
||||
)
|
||||
|
||||
candle_limit = self._exchange.ohlcv_candle_limit(
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
from datetime import timedelta
|
||||
from typing import Any, Literal
|
||||
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.constants import DOCS_LINK, ListPairsWithTimeframes
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||
from freqtrade.exchange.exchange_types import Tickers
|
||||
@@ -35,19 +35,19 @@ class VolumePairList(IPairList):
|
||||
'for "pairlist.config.number_assets"'
|
||||
)
|
||||
|
||||
self._stake_currency = self._config["stake_currency"]
|
||||
self._number_pairs = self._pairlistconfig["number_assets"]
|
||||
self._stake_currency: str = self._config["stake_currency"]
|
||||
self._number_pairs: int = 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._min_value: float | None = self._pairlistconfig.get("min_value", 0)
|
||||
self._max_value: float | None = self._pairlistconfig.get("max_value", None)
|
||||
self._refresh_period = self._pairlistconfig.get("refresh_period", 1800)
|
||||
self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1, ttl=self._refresh_period)
|
||||
self._lookback_days = self._pairlistconfig.get("lookback_days", 0)
|
||||
self._lookback_timeframe = self._pairlistconfig.get("lookback_timeframe", "1d")
|
||||
self._lookback_period = self._pairlistconfig.get("lookback_period", 0)
|
||||
self._lookback_days: int = self._pairlistconfig.get("lookback_days", 0)
|
||||
self._lookback_timeframe: str = self._pairlistconfig.get("lookback_timeframe", "1d")
|
||||
self._lookback_period: int = self._pairlistconfig.get("lookback_period", 0)
|
||||
self._def_candletype = self._config["candle_type_def"]
|
||||
|
||||
if (self._lookback_days > 0) & (self._lookback_period > 0):
|
||||
if (self._lookback_days > 0) and (self._lookback_period > 0):
|
||||
raise OperationalException(
|
||||
"Ambiguous configuration: lookback_days and lookback_period both set in pairlist "
|
||||
"config. Please set lookback_days only or lookback_period and lookback_timeframe "
|
||||
@@ -64,9 +64,9 @@ class VolumePairList(IPairList):
|
||||
_tf_in_sec = self._tf_in_min * 60
|
||||
|
||||
# whether to use range lookback or not
|
||||
self._use_range = (self._tf_in_min > 0) & (self._lookback_period > 0)
|
||||
self._use_range = (self._tf_in_min > 0) and (self._lookback_period > 0)
|
||||
|
||||
if self._use_range & (self._refresh_period < _tf_in_sec):
|
||||
if self._use_range and (self._refresh_period < _tf_in_sec):
|
||||
raise OperationalException(
|
||||
f"Refresh period of {self._refresh_period} seconds is smaller than one "
|
||||
f"timeframe of {self._lookback_timeframe}. Please adjust refresh_period "
|
||||
@@ -78,9 +78,11 @@ class VolumePairList(IPairList):
|
||||
and self._exchange.get_option("tickers_have_quoteVolume")
|
||||
):
|
||||
raise OperationalException(
|
||||
"Exchange does not support dynamic whitelist in this configuration. "
|
||||
"Please edit your config and either remove Volumepairlist, "
|
||||
"or switch to using candles. and restart the bot."
|
||||
f"Exchange {self._exchange.name} does not support dynamic whitelist in this "
|
||||
"configuration. Please edit your config and either remove Volumepairlist, "
|
||||
"or switch to using candles and restart the bot. "
|
||||
f"You can find more information about this in the documentation under "
|
||||
f"{DOCS_LINK}/plugins/#volumepairlist-advanced-mode ."
|
||||
)
|
||||
|
||||
if not self._validate_keys(self._sort_key):
|
||||
@@ -297,7 +299,7 @@ class VolumePairList(IPairList):
|
||||
# Tickers mode - filter based on incoming pairlist.
|
||||
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
|
||||
|
||||
if self._min_value > 0:
|
||||
if self._min_value and 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]
|
||||
|
||||
@@ -51,8 +51,8 @@ class PairListManager(LoggingMixin):
|
||||
invalid = ". ".join([p.name for p in self._pairlist_handlers if p.needstickers])
|
||||
|
||||
raise OperationalException(
|
||||
"Exchange does not support fetchTickers, therefore the following pairlists "
|
||||
"cannot be used. Please edit your config and restart the bot.\n"
|
||||
f"Exchange {self._exchange.name} does not support fetchTickers, therefore the "
|
||||
"following pairlists cannot be used. Please edit your config and restart the bot.\n"
|
||||
f"{invalid}."
|
||||
)
|
||||
|
||||
|
||||
@@ -47,13 +47,17 @@ class ProtectionManager:
|
||||
"""
|
||||
return [{p.name: p.short_desc()} for p in self._protection_handlers]
|
||||
|
||||
def global_stop(self, now: datetime | None = None, side: LongShort = "long") -> PairLock | None:
|
||||
def global_stop(
|
||||
self, now: datetime | None = None, side: LongShort = "long", starting_balance: float = 0.0
|
||||
) -> PairLock | None:
|
||||
if not now:
|
||||
now = datetime.now(UTC)
|
||||
result = None
|
||||
for protection_handler in self._protection_handlers:
|
||||
if protection_handler.has_global_stop:
|
||||
lock = protection_handler.global_stop(date_now=now, side=side)
|
||||
lock = protection_handler.global_stop(
|
||||
date_now=now, side=side, starting_balance=starting_balance
|
||||
)
|
||||
if lock and lock.until:
|
||||
if not PairLocks.is_global_lock(lock.until, side=lock.lock_side):
|
||||
result = PairLocks.lock_pair(
|
||||
@@ -62,14 +66,20 @@ class ProtectionManager:
|
||||
return result
|
||||
|
||||
def stop_per_pair(
|
||||
self, pair, now: datetime | None = None, side: LongShort = "long"
|
||||
self,
|
||||
pair,
|
||||
now: datetime | None = None,
|
||||
side: LongShort = "long",
|
||||
starting_balance: float = 0.0,
|
||||
) -> PairLock | None:
|
||||
if not now:
|
||||
now = datetime.now(UTC)
|
||||
result = None
|
||||
for protection_handler in self._protection_handlers:
|
||||
if protection_handler.has_local_stop:
|
||||
lock = protection_handler.stop_per_pair(pair=pair, date_now=now, side=side)
|
||||
lock = protection_handler.stop_per_pair(
|
||||
pair=pair, date_now=now, side=side, starting_balance=starting_balance
|
||||
)
|
||||
if lock and lock.until:
|
||||
if not PairLocks.is_pair_locked(pair, lock.until, lock.lock_side):
|
||||
result = PairLocks.lock_pair(
|
||||
|
||||
@@ -52,7 +52,9 @@ class CooldownPeriod(IProtection):
|
||||
|
||||
return None
|
||||
|
||||
def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None:
|
||||
def global_stop(
|
||||
self, date_now: datetime, side: LongShort, starting_balance: float
|
||||
) -> ProtectionReturn | None:
|
||||
"""
|
||||
Stops trading (position entering) for all pairs
|
||||
This must evaluate to true for the whole period of the "cooldown period".
|
||||
@@ -63,7 +65,7 @@ class CooldownPeriod(IProtection):
|
||||
return None
|
||||
|
||||
def stop_per_pair(
|
||||
self, pair: str, date_now: datetime, side: LongShort
|
||||
self, pair: str, date_now: datetime, side: LongShort, starting_balance: float
|
||||
) -> ProtectionReturn | None:
|
||||
"""
|
||||
Stops trading (position entering) for this pair
|
||||
|
||||
@@ -102,7 +102,9 @@ class IProtection(LoggingMixin, ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None:
|
||||
def global_stop(
|
||||
self, date_now: datetime, side: LongShort, starting_balance: float
|
||||
) -> ProtectionReturn | None:
|
||||
"""
|
||||
Stops trading (position entering) for all pairs
|
||||
This must evaluate to true for the whole period of the "cooldown period".
|
||||
@@ -110,7 +112,7 @@ class IProtection(LoggingMixin, ABC):
|
||||
|
||||
@abstractmethod
|
||||
def stop_per_pair(
|
||||
self, pair: str, date_now: datetime, side: LongShort
|
||||
self, pair: str, date_now: datetime, side: LongShort, starting_balance: float
|
||||
) -> ProtectionReturn | None:
|
||||
"""
|
||||
Stops trading (position entering) for this pair
|
||||
|
||||
@@ -81,7 +81,9 @@ class LowProfitPairs(IProtection):
|
||||
|
||||
return None
|
||||
|
||||
def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None:
|
||||
def global_stop(
|
||||
self, date_now: datetime, side: LongShort, starting_balance: float
|
||||
) -> ProtectionReturn | None:
|
||||
"""
|
||||
Stops trading (position entering) for all pairs
|
||||
This must evaluate to true for the whole period of the "cooldown period".
|
||||
@@ -91,7 +93,7 @@ class LowProfitPairs(IProtection):
|
||||
return None
|
||||
|
||||
def stop_per_pair(
|
||||
self, pair: str, date_now: datetime, side: LongShort
|
||||
self, pair: str, date_now: datetime, side: LongShort, starting_balance: float
|
||||
) -> ProtectionReturn | None:
|
||||
"""
|
||||
Stops trading (position entering) for this pair
|
||||
|
||||
@@ -22,6 +22,7 @@ class MaxDrawdown(IProtection):
|
||||
|
||||
self._trade_limit = protection_config.get("trade_limit", 1)
|
||||
self._max_allowed_drawdown = protection_config.get("max_allowed_drawdown", 0.0)
|
||||
self._calculation_mode = protection_config.get("calculation_mode", "ratios")
|
||||
# TODO: Implement checks to limit max_drawdown to sensible values
|
||||
|
||||
def short_desc(self) -> str:
|
||||
@@ -42,25 +43,53 @@ class MaxDrawdown(IProtection):
|
||||
f"locking {self.unlock_reason_time_element}."
|
||||
)
|
||||
|
||||
def _max_drawdown(self, date_now: datetime) -> ProtectionReturn | None:
|
||||
def _max_drawdown(self, date_now: datetime, starting_balance: float) -> ProtectionReturn | None:
|
||||
"""
|
||||
Evaluate recent trades for drawdown ...
|
||||
"""
|
||||
look_back_until = date_now - timedelta(minutes=self._lookback_period)
|
||||
|
||||
trades = Trade.get_trades_proxy(is_open=False, close_date=look_back_until)
|
||||
trades_in_window = Trade.get_trades_proxy(is_open=False, close_date=look_back_until)
|
||||
|
||||
trades_df = pd.DataFrame([trade.to_json() for trade in trades])
|
||||
|
||||
if len(trades) < self._trade_limit:
|
||||
# Not enough trades in the relevant period
|
||||
if len(trades_in_window) < self._trade_limit:
|
||||
return None
|
||||
|
||||
# Drawdown is always positive
|
||||
try:
|
||||
# TODO: This should use absolute profit calculation, considering account balance.
|
||||
drawdown_obj = calculate_max_drawdown(trades_df, value_col="close_profit")
|
||||
drawdown = drawdown_obj.drawdown_abs
|
||||
if self._calculation_mode == "equity":
|
||||
# Standard equity-based drawdown
|
||||
# Get all trades to calculate cumulative profit before the window
|
||||
all_closed_trades = Trade.get_trades_proxy(is_open=False)
|
||||
profit_before_window = sum(
|
||||
trade.close_profit_abs or 0.0
|
||||
for trade in all_closed_trades
|
||||
if trade.close_date_utc <= look_back_until
|
||||
)
|
||||
|
||||
trades_df = pd.DataFrame(
|
||||
[
|
||||
{"close_date": t.close_date_utc, "profit_abs": t.close_profit_abs}
|
||||
for t in trades_in_window
|
||||
]
|
||||
)
|
||||
actual_starting_balance = starting_balance + profit_before_window
|
||||
drawdown_obj = calculate_max_drawdown(
|
||||
trades_df,
|
||||
value_col="profit_abs",
|
||||
starting_balance=actual_starting_balance,
|
||||
relative=True,
|
||||
)
|
||||
drawdown = drawdown_obj.relative_account_drawdown
|
||||
else:
|
||||
# Legacy ratios-based calculation (default)
|
||||
trades_df = pd.DataFrame(
|
||||
[
|
||||
{"close_date": t.close_date_utc, "close_profit": t.close_profit}
|
||||
for t in trades_in_window
|
||||
]
|
||||
)
|
||||
drawdown_obj = calculate_max_drawdown(trades_df, value_col="close_profit")
|
||||
# In ratios mode, drawdown_abs is the cumulative ratio drop
|
||||
drawdown = drawdown_obj.drawdown_abs
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@@ -71,7 +100,7 @@ class MaxDrawdown(IProtection):
|
||||
logger.info,
|
||||
)
|
||||
|
||||
until = self.calculate_lock_end(trades)
|
||||
until = self.calculate_lock_end(trades_in_window)
|
||||
|
||||
return ProtectionReturn(
|
||||
lock=True,
|
||||
@@ -81,17 +110,19 @@ class MaxDrawdown(IProtection):
|
||||
|
||||
return None
|
||||
|
||||
def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None:
|
||||
def global_stop(
|
||||
self, date_now: datetime, side: LongShort, starting_balance: float
|
||||
) -> ProtectionReturn | None:
|
||||
"""
|
||||
Stops trading (position entering) for all pairs
|
||||
This must evaluate to true for the whole period of the "cooldown period".
|
||||
:return: Tuple of [bool, until, reason].
|
||||
If true, all pairs will be locked with <reason> until <until>
|
||||
"""
|
||||
return self._max_drawdown(date_now)
|
||||
return self._max_drawdown(date_now, starting_balance)
|
||||
|
||||
def stop_per_pair(
|
||||
self, pair: str, date_now: datetime, side: LongShort
|
||||
self, pair: str, date_now: datetime, side: LongShort, starting_balance: float
|
||||
) -> ProtectionReturn | None:
|
||||
"""
|
||||
Stops trading (position entering) for this pair
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user