Compare commits
491 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ee500db4f | |||
| b3296a06d9 | |||
| 09051fb641 | |||
| 3c6500c2bc | |||
| f242110957 | |||
| 9480331b45 | |||
| 7c5b2fdffb | |||
| 8cf3f3826f | |||
| 799ce4e1f2 | |||
| 1fdce89806 | |||
| e1f2f0cb64 | |||
| c3b6258dfe | |||
| 4154a7058d | |||
| bdfa9c146e | |||
| c6d6bc62eb | |||
| 2347195b0b | |||
| f3101e8a75 | |||
| 43dd90f6d4 | |||
| 7ef52365f7 | |||
| 00bdf13fae | |||
| 5f8608d3b3 | |||
| d19498667a | |||
| 286371c1e4 | |||
| 3ffc69ad02 | |||
| 7b44dd17ae | |||
| 5b0b44069a | |||
| 85fc936431 | |||
| 04a28b2550 | |||
| 9fa3a15e24 | |||
| 23b10161d5 | |||
| 31e4501765 | |||
| c6e64bad49 | |||
| 8e2de9ef7e | |||
| 5ea7ba6b9a | |||
| e3e924d888 | |||
| 02b0f0abd6 | |||
| 7735ea91bb | |||
| 7178ce8559 | |||
| 26ba076143 | |||
| 425701ddcf | |||
| 42e45a0a65 | |||
| b97f3ca818 | |||
| dfc77945ce | |||
| 6d8011e075 | |||
| 1b4f8dfa54 | |||
| 48b7a85c90 | |||
| 0d7854ff1b | |||
| d3464ac2dc | |||
| 95f5db9dcd | |||
| 61b29962c4 | |||
| b83754c5a4 | |||
| 9a1f2d42a7 | |||
| bd511c2158 | |||
| 6b6d7fc310 | |||
| c6959c92ac | |||
| 7bc6a2279c | |||
| b431c5b343 | |||
| 176f5518ea | |||
| 06406b7103 | |||
| 17e4f5ed1f | |||
| 578ba9ea4a | |||
| 83a8651d41 | |||
| ba0c22b6f0 | |||
| 90219f040b | |||
| fef234d80f | |||
| 68ad688665 | |||
| 0c7a2747d3 | |||
| 23187f0c41 | |||
| f66d81c4b8 | |||
| 87a64cbe68 | |||
| ef58aaf9e9 | |||
| 4a432760ed | |||
| 484943a640 | |||
| eec16cfc8a | |||
| fddceb2572 | |||
| d665a01fe6 | |||
| 8f93ac5770 | |||
| 33164ec041 | |||
| 0e6cd72edd | |||
| 4f218e4fb2 | |||
| 0bf411ae19 | |||
| 283199b6ed | |||
| 0270a61cba | |||
| 1aeed3951b | |||
| ba38f8120c | |||
| 7de9e2c4b6 | |||
| 331c6beafc | |||
| 93cbbbbe31 | |||
| ddd2e2cc3b | |||
| 55301b8d0c | |||
| a47ed1dbdb | |||
| 17b161cb03 | |||
| 5d3c764e27 | |||
| f3aac3d66d | |||
| 39903d521e | |||
| 7255beddea | |||
| a95c3ace92 | |||
| d543feb032 | |||
| 623f27341f | |||
| 55dc2d4bf4 | |||
| 24e94cfaa7 | |||
| 7db62689c4 | |||
| da53b5d115 | |||
| 03dfe4ec45 | |||
| 39288d2e53 | |||
| 2a6dc3e45d | |||
| 5964845265 | |||
| 9911d2aa32 | |||
| bfd2c3f942 | |||
| 74c837519a | |||
| 415e361c66 | |||
| 7e154c6fb6 | |||
| 385b39ff66 | |||
| 9d232c4949 | |||
| 85ccc31a63 | |||
| 65d19c38b6 | |||
| 5b8752e649 | |||
| 8832bae371 | |||
| a85e044e99 | |||
| 7b1ee84b34 | |||
| 79ebc943a2 | |||
| eabcaa38e2 | |||
| e930221b78 | |||
| 08c4f24bdf | |||
| 56c23f9dd8 | |||
| 6edee269d8 | |||
| f9d978f16a | |||
| 1eac77fe0b | |||
| a98121ea26 | |||
| cc9c373a76 | |||
| 390b113776 | |||
| bf33f8b632 | |||
| 030dcfdd83 | |||
| cd77758852 | |||
| 1aa6c2ad55 | |||
| 55d71cecdd | |||
| 70a81c86ba | |||
| 1c6c710696 | |||
| 4ca2a043b5 | |||
| 41418784e3 | |||
| 9afd572948 | |||
| 51a99f2a66 | |||
| 969b0570c2 | |||
| e94ccece93 | |||
| cd435098bd | |||
| 33659ee273 | |||
| c30db9d0d6 | |||
| 429505b134 | |||
| 2c2cc086c3 | |||
| fe16654733 | |||
| f85891941f | |||
| 493b6f6592 | |||
| 97faa7fc5a | |||
| 5402b14336 | |||
| 4c7ff7ab0c | |||
| 8288a5cf91 | |||
| 442b29e0ea | |||
| 743422ccf3 | |||
| d69b1566b1 | |||
| 5c01023d55 | |||
| 97d303579a | |||
| 265a798f78 | |||
| db19446e27 | |||
| e62a0f76ed | |||
| 673447794e | |||
| 76aefccd03 | |||
| b0b9e398e1 | |||
| fac0491658 | |||
| 7770f082c8 | |||
| 3281049264 | |||
| fe48f6769f | |||
| 6a5be68367 | |||
| 6613366343 | |||
| 48c1aaaa38 | |||
| 23928e55a3 | |||
| 6d84b93715 | |||
| 5b1011668a | |||
| 30d2298d54 | |||
| 98b6f58eb2 | |||
| a0cbdc9135 | |||
| 805e03b83b | |||
| 35cc7da9b3 | |||
| 6efe6bbfc2 | |||
| ac29ef67fd | |||
| 4a7140c05d | |||
| cd971cff4f | |||
| 538139ef31 | |||
| 8bd4926c29 | |||
| a9195c8ff9 | |||
| 93c8a11824 | |||
| aec496a73b | |||
| 7bc1398574 | |||
| 3371bad504 | |||
| 64e27637dc | |||
| ee4f05a5b8 | |||
| 6636a2fda8 | |||
| 754936d253 | |||
| 3762f40538 | |||
| 4a3c45af2d | |||
| 29fdbc8f7f | |||
| 6116c6abb9 | |||
| 466c21e9fd | |||
| d461d6f8e8 | |||
| 53f68a3834 | |||
| 76d43782db | |||
| e7a6db8916 | |||
| cdb7fa83e4 | |||
| f785f9ff00 | |||
| 4d87d13165 | |||
| 9128011208 | |||
| 3dd7f7f17b | |||
| 7665d3d1f2 | |||
| e055feb1d3 | |||
| dfe6982099 | |||
| ad2aef09c4 | |||
| facfdfd85c | |||
| 1bba2a5f21 | |||
| ef239b8d0c | |||
| bccbac9f23 | |||
| 113efe129d | |||
| 8d73cccde5 | |||
| 055c56edfc | |||
| 942c1c1f5c | |||
| 243a91a141 | |||
| 281bd69fa5 | |||
| 6630259833 | |||
| 674d28622b | |||
| e21aedfb73 | |||
| 8f22c58875 | |||
| e3d1c4c9c7 | |||
| 40f87c9ab5 | |||
| bd5f02a8df | |||
| 91438affc8 | |||
| 1548fdfca6 | |||
| 4bebb20477 | |||
| 3fb4524c5a | |||
| cdd1273ed0 | |||
| 717f17a9b5 | |||
| 11763e8cd1 | |||
| 5e50348150 | |||
| ccfc690281 | |||
| e1f6702932 | |||
| 13e9f8a98e | |||
| cc0337585e | |||
| 3070e37335 | |||
| c50edafac6 | |||
| 7ccac492bb | |||
| a23409b72f | |||
| 5b9102f9a8 | |||
| f447de4840 | |||
| 186bc4d272 | |||
| 06fd176286 | |||
| c5fd6f4519 | |||
| efe0275ae1 | |||
| ba98f1a9be | |||
| 5380b22729 | |||
| 338bc7c677 | |||
| 449a5e85b4 | |||
| 0f5cb87c65 | |||
| 822e7bad1f | |||
| 86a5240174 | |||
| 7b7c8d9374 | |||
| fde3547dbb | |||
| 9bd7fac86f | |||
| 8085d8cd00 | |||
| 3f656a9758 | |||
| 38f1981d6f | |||
| d55b3e0228 | |||
| d3f8bef2ef | |||
| f78f52f78e | |||
| d451dcf28b | |||
| 063964ed38 | |||
| ac70d559a4 | |||
| 685e575f6b | |||
| 1640267798 | |||
| d5585dd5e3 | |||
| 90e2db708e | |||
| 64303e90d6 | |||
| 901dfa21fc | |||
| 5e7d883732 | |||
| 840f074c66 | |||
| 0408ad1095 | |||
| e262110c3b | |||
| 09b9ff2c68 | |||
| e76574b79f | |||
| 08587d826e | |||
| 584b84a941 | |||
| 44182783c0 | |||
| 90f52ba8ad | |||
| f8f10f27e9 | |||
| 7f393252e2 | |||
| dd8938ced2 | |||
| 67ce9a41f2 | |||
| 756bada570 | |||
| 25c0036012 | |||
| 3b03fae939 | |||
| 0f9e61371c | |||
| 1970cc65c0 | |||
| 8d74e8b8dd | |||
| fc4e09f398 | |||
| a45d941b11 | |||
| 39fd27c7ec | |||
| fa939a7f66 | |||
| 6fc81fa9c7 | |||
| 89cd46d2e5 | |||
| f287d0ad24 | |||
| 93e3bbea5a | |||
| 6be25bd86a | |||
| ec82894990 | |||
| ea1b7c692a | |||
| 0e08173235 | |||
| cdf33761c0 | |||
| 63ca07aeac | |||
| 5022407021 | |||
| f54b7d56d3 | |||
| 382befb053 | |||
| 11404ef230 | |||
| 8964cfc123 | |||
| ec97723c54 | |||
| 4cfbcefb33 | |||
| 1f575d95e6 | |||
| d4900c28e1 | |||
| f12964f543 | |||
| c339853ec0 | |||
| 3924514d2b | |||
| aae92c29db | |||
| 5683063379 | |||
| 5ab993b6d5 | |||
| 9584f6c549 | |||
| 2a95a3aefe | |||
| 36eeac365e | |||
| 2b8c4942fd | |||
| 4a190bd13c | |||
| cf6a89e161 | |||
| a29c5322ab | |||
| 4e2d2639ae | |||
| a54f895ea9 | |||
| ad55615ecb | |||
| c8aff5687d | |||
| 02c2e451b1 | |||
| 84d49a0f53 | |||
| 22b096be25 | |||
| b285cdd491 | |||
| b2881affc0 | |||
| b3e0b42155 | |||
| 314c017dd3 | |||
| 9fbc413d4d | |||
| ad96fa0f58 | |||
| f2283410ce | |||
| f14c4ebcc8 | |||
| ceff757bc6 | |||
| c3c6207cda | |||
| 975ad26eb8 | |||
| 8a5ff4ed0d | |||
| 7d19e0e692 | |||
| a1a5cab04e | |||
| 8182947f29 | |||
| 2231ba3f04 | |||
| bf0a08f5a7 | |||
| 552575c7e6 | |||
| d0979d560f | |||
| e3a9b0aa17 | |||
| 7caa22c7a7 | |||
| 91131d7884 | |||
| 967ad2f8ff | |||
| d00f82a641 | |||
| 2f83f07fd3 | |||
| 78d213a8a0 | |||
| 44d798239d | |||
| 08e1a616a0 | |||
| 8009c03629 | |||
| ae068bd6f5 | |||
| 2350aa6a27 | |||
| 8394826d62 | |||
| a46d41a594 | |||
| a0d8055917 | |||
| 3ec892a5ab | |||
| 3deffb57ce | |||
| db8c1917e0 | |||
| 57f55d416c | |||
| 0b4a6e7e11 | |||
| f918a66292 | |||
| d795af5921 | |||
| 21b8982e55 | |||
| 5a9f66e6d6 | |||
| 4fd77e7ee7 | |||
| 428ca972e2 | |||
| 3913e4fa96 | |||
| e296242072 | |||
| 82001d1e8b | |||
| 137bfc2b56 | |||
| a8ae767576 | |||
| b9c77124fc | |||
| f060548134 | |||
| 18ce8af084 | |||
| 7f7cff85c1 | |||
| 9a22e5c775 | |||
| 2cfb8834b6 | |||
| 013006050f | |||
| 99df346bf5 | |||
| 2e57158556 | |||
| b75eb85e66 | |||
| dbb9f225d0 | |||
| 85753be7cb | |||
| 18b35b0657 | |||
| 857aeaa9eb | |||
| 3e06971038 | |||
| 323b9f8693 | |||
| a412ff76bc | |||
| 2bfd66d271 | |||
| 8cad9c8436 | |||
| 3504073428 | |||
| b4af89380b | |||
| c131493eca | |||
| 37031ba83d | |||
| 69106633cd | |||
| a2e674b237 | |||
| c1c9f0151e | |||
| 963ec23ae2 | |||
| 0c35f3a47a | |||
| f4fdcfc78d | |||
| 97c67d55c6 | |||
| 9983b44e6f | |||
| 78bf9bf60d | |||
| 9d22dac28d | |||
| 37e16acc80 | |||
| 6d5cd69764 | |||
| 3a8c594ae8 | |||
| 5f72ed9b3c | |||
| bee897f5b7 | |||
| 75542cdd93 | |||
| 97da78c499 | |||
| d8d35ec234 | |||
| 86d65f8428 | |||
| 973aa6a0c3 | |||
| aaab2a1c86 | |||
| 16924525ff | |||
| f1f355330c | |||
| 4c5c054bca | |||
| 0181e00c19 | |||
| ecf013dd18 | |||
| 59837ef6ac | |||
| b42a91d783 | |||
| 187c8b8070 | |||
| a807d2d6c8 | |||
| fda7b0f1ef | |||
| ae42f8d512 | |||
| 6fb74fa166 | |||
| 7dd0f2174c | |||
| e1cc7dcfdf | |||
| 6b3fff6b7e | |||
| 5d4324dd55 | |||
| fc0cae21e6 | |||
| 89b6cc89a3 | |||
| 26af62e4dd | |||
| f90cd44a9e | |||
| 35f7c97c76 | |||
| d530527f11 | |||
| 8af8260b7c | |||
| 7a17cd781a | |||
| ba244cd2f6 | |||
| 7d3c8b48dd | |||
| 6931b81daa | |||
| 92405f94ec | |||
| 3e1daf50b0 | |||
| 0caeb6b5f7 | |||
| 85d235958e | |||
| 008f875282 | |||
| 8b42c2558a | |||
| 40f56e43cc | |||
| e9c961802b | |||
| 948b6e041d | |||
| 80d776af56 | |||
| e62d84e35d | |||
| 098a6551f6 | |||
| 6e15fd0da6 | |||
| 1f2dadc333 | |||
| c3b7a2b382 | |||
| 3746305b5a | |||
| af3f9d5a75 | |||
| 7d5f021717 | |||
| 9207cf501c | |||
| fc1c3a8f97 | |||
| 83e56a09c2 | |||
| 47613b1cf9 | |||
| ef3a7d5c92 | |||
| 705d1e4cc0 | |||
| 5b3f348bbb | |||
| aa81c75bef | |||
| 6b889814ad | |||
| 1ade11f00b |
@@ -16,6 +16,8 @@ jobs:
|
||||
name: develop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -42,6 +44,7 @@ jobs:
|
||||
branch: update/binance-leverage-tiers
|
||||
title: Update Binance Leverage Tiers
|
||||
commit-message: "chore: update pre-commit hooks"
|
||||
committer: Freqtrade Bot <noreply@github.com>
|
||||
committer: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||
author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||
body: Update binance leverage tiers.
|
||||
delete-branch: true
|
||||
|
||||
@@ -24,11 +24,13 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ "ubuntu-20.04", "ubuntu-22.04", "ubuntu-24.04" ]
|
||||
os: [ "ubuntu-22.04", "ubuntu-24.04" ]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -157,6 +159,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -281,6 +285,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -366,6 +372,8 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -381,6 +389,8 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -391,6 +401,8 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Documentation syntax
|
||||
run: |
|
||||
@@ -420,6 +432,8 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -506,6 +520,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -551,6 +567,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download artifact 📦
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -578,6 +596,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download artifact 📦
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -598,6 +618,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -633,7 +655,9 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
run: echo ${PLATFORMS}
|
||||
env:
|
||||
PLATFORMS: ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Build and test and push docker images
|
||||
env:
|
||||
@@ -652,6 +676,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract-branch
|
||||
|
||||
@@ -20,6 +20,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -43,12 +45,16 @@ jobs:
|
||||
- name: Build and push Mike
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: |
|
||||
mike deploy ${{ github.ref_name }} latest --push --update-aliases
|
||||
mike deploy ${REF_NAME} latest --push --update-aliases
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
|
||||
- name: Build and push Mike - Release
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
run: |
|
||||
mike deploy ${{ github.ref_name }} stable --push --update-aliases
|
||||
mike deploy ${REF_NAME} stable --push --update-aliases
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
|
||||
- name: Show mike versions
|
||||
run: |
|
||||
|
||||
@@ -17,29 +17,26 @@ concurrency:
|
||||
group: "${{ github.workflow }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
permissions:
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Pre-build dev container image
|
||||
uses: devcontainers/ci@v0.3
|
||||
with:
|
||||
subFolder: .github
|
||||
imageName: ghcr.io/${{ github.repository }}-devcontainer
|
||||
cacheFrom: ghcr.io/${{ github.repository }}-devcontainer
|
||||
push: always
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Pre-build dev container image
|
||||
uses: devcontainers/ci@v0.3
|
||||
with:
|
||||
subFolder: .github
|
||||
imageName: ghcr.io/${{ github.repository }}-devcontainer
|
||||
cacheFrom: ghcr.io/${{ github.repository }}-devcontainer
|
||||
push: always
|
||||
|
||||
@@ -4,11 +4,16 @@ on:
|
||||
branches:
|
||||
- stable
|
||||
|
||||
# disable permissions for all of the available permissions
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
dockerHubDescription:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
|
||||
@@ -14,6 +14,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -36,6 +38,7 @@ jobs:
|
||||
branch: update/pre-commit-hooks
|
||||
title: Update pre-commit hooks
|
||||
commit-message: "chore: update pre-commit hooks"
|
||||
committer: Freqtrade Bot <noreply@github.com>
|
||||
committer: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||
author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||
body: Update versions of pre-commit hooks to latest version.
|
||||
delete-branch: true
|
||||
|
||||
+14
-8
@@ -2,28 +2,28 @@
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: "7.1.1"
|
||||
rev: "7.1.2"
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [Flake8-pyproject]
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.14.1"
|
||||
rev: "v1.15.0"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==5.5.0.20240820
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.32.0.20241016
|
||||
- types-requests==2.32.0.20250306
|
||||
- types-tabulate==0.9.0.20241207
|
||||
- types-python-dateutil==2.9.0.20241206
|
||||
- SQLAlchemy==2.0.37
|
||||
- SQLAlchemy==2.0.39
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: "6.0.0"
|
||||
rev: "6.0.1"
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
@@ -31,7 +31,7 @@ repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.9.3'
|
||||
rev: 'v0.11.2'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
@@ -57,13 +57,19 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/stefmolin/exif-stripper
|
||||
rev: 0.6.1
|
||||
rev: 0.6.2
|
||||
hooks:
|
||||
- id: strip-exif
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.4.0
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies:
|
||||
- tomli
|
||||
|
||||
# Ensure github actions remain safe
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.5.2
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12.7-slim-bookworm as base
|
||||
FROM python:3.12.9-slim-bookworm as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 
|
||||
|
||||
[](https://github.com/freqtrade/freqtrade/actions/)
|
||||
[](https://github.com/freqtrade/freqtrade/actions/)
|
||||
[](https://doi.org/10.21105/joss.04864)
|
||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||
[](https://www.freqtrade.io)
|
||||
|
||||
BIN
Binary file not shown.
@@ -257,7 +257,8 @@
|
||||
"enum": [
|
||||
"day",
|
||||
"week",
|
||||
"month"
|
||||
"month",
|
||||
"year"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -541,6 +542,10 @@
|
||||
"description": "Edge configuration.",
|
||||
"$ref": "#/definitions/edge"
|
||||
},
|
||||
"log_config": {
|
||||
"description": "Logging configuration.",
|
||||
"$ref": "#/definitions/logging"
|
||||
},
|
||||
"freqai": {
|
||||
"description": "FreqAI configuration.",
|
||||
"$ref": "#/definitions/freqai"
|
||||
@@ -612,6 +617,14 @@
|
||||
"description": "Telegram topic ID - only applicable for group chats",
|
||||
"type": "string"
|
||||
},
|
||||
"authorized_users": {
|
||||
"description": "Authorized users for the bot.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"allow_custom_messages": {
|
||||
"description": "Allow sending custom messages from the Strategy.",
|
||||
"type": "boolean",
|
||||
@@ -1272,6 +1285,30 @@
|
||||
"allowed_risk"
|
||||
]
|
||||
},
|
||||
"logging": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "number",
|
||||
"const": 1
|
||||
},
|
||||
"formatters": {
|
||||
"type": "object"
|
||||
},
|
||||
"handlers": {
|
||||
"type": "object"
|
||||
},
|
||||
"root": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"version",
|
||||
"formatters",
|
||||
"handlers",
|
||||
"root"
|
||||
]
|
||||
},
|
||||
"external_message_consumer": {
|
||||
"description": "Configuration for external message consumer.",
|
||||
"type": "object",
|
||||
@@ -1366,10 +1403,10 @@
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"keras": {
|
||||
"description": "Use Keras for model training.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
"identifier": {
|
||||
"description": "A unique ID for the current model. Must be changed when modifying features.",
|
||||
"type": "string",
|
||||
"default": "example"
|
||||
},
|
||||
"write_metrics_to_disk": {
|
||||
"description": "Write metrics to disk?",
|
||||
@@ -1399,16 +1436,49 @@
|
||||
"type": "number",
|
||||
"default": 7
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique ID for the current model. Must be changed when modifying features.",
|
||||
"type": "string",
|
||||
"default": "example"
|
||||
"live_retrain_hours": {
|
||||
"description": "Frequency of retraining during dry/live runs.",
|
||||
"type": "number",
|
||||
"default": 0
|
||||
},
|
||||
"expiration_hours": {
|
||||
"description": "Avoid making predictions if a model is more than `expiration_hours` old. Defaults to 0 (no expiration).",
|
||||
"type": "number",
|
||||
"default": 0
|
||||
},
|
||||
"save_backtest_models": {
|
||||
"description": "Save models to disk when running backtesting.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"fit_live_predictions_candles": {
|
||||
"description": "Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset.",
|
||||
"type": "integer"
|
||||
},
|
||||
"data_kitchen_thread_count": {
|
||||
"description": "Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.).",
|
||||
"type": "integer"
|
||||
},
|
||||
"activate_tensorboard": {
|
||||
"description": "Indicate whether or not to activate tensorboard",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"wait_for_training_iteration_on_reload": {
|
||||
"description": "Wait for the next training iteration to complete after /reload or ctrl+c.",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"continual_learning": {
|
||||
"description": "Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"keras": {
|
||||
"description": "Use Keras for model training.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"feature_parameters": {
|
||||
"description": "The parameters used to engineer the feature set",
|
||||
"type": "object",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11.10-slim-bookworm as base
|
||||
FROM python:3.11.11-slim-bookworm as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
@@ -34,7 +34,7 @@ COPY build_helpers/* /tmp/
|
||||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
||||
USER ftuser
|
||||
RUN pip install --user --no-cache-dir numpy \
|
||||
RUN pip install --user --no-cache-dir "numpy<2" \
|
||||
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib \
|
||||
&& pip install --user --no-cache-dir -r requirements.txt
|
||||
|
||||
|
||||
+198
-13
@@ -188,30 +188,111 @@ as the watchdog.
|
||||
|
||||
## Advanced Logging
|
||||
|
||||
Freqtrade uses the default logging module provided by python.
|
||||
Python allows for extensive [logging configuration](https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig) in this regards - way more than what can be covered here.
|
||||
|
||||
Default logging (Colored terminal output) is setup by default if no `log_config` is provided.
|
||||
Using `--logfile logfile.log` will enable the RotatingFileHandler.
|
||||
If you're not content with the log format - or with the default settings provided for the RotatingFileHandler, you can customize logging to your liking.
|
||||
|
||||
The default configuration looks roughly like the below - with the file handler being provided - but not enabled.
|
||||
|
||||
``` json hl_lines="5-7 13-16 27"
|
||||
{
|
||||
"log_config": {
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"basic": {
|
||||
"format": "%(message)s"
|
||||
},
|
||||
"standard": {
|
||||
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "freqtrade.loggers.ft_rich_handler.FtRichHandler",
|
||||
"formatter": "basic"
|
||||
},
|
||||
"file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"formatter": "standard",
|
||||
// "filename": "someRandomLogFile.log",
|
||||
"maxBytes": 10485760,
|
||||
"backupCount": 10
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"handlers": [
|
||||
"console",
|
||||
// "file"
|
||||
],
|
||||
"level": "INFO",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! Note "highlighted lines"
|
||||
Highlighted lines in the above code-block define the Rich handler and belong together.
|
||||
The formatter "standard" and "file" will belong to the FileHandler.
|
||||
|
||||
Each handler must use one of the defined formatters (by name) - and it's class must be available and a valid logging class.
|
||||
To actually use a handler - it must be in the "handlers" section inside the "root" segment.
|
||||
If this section is left out, freqtrade will provide no output (in the non-configured handler, anyway).
|
||||
|
||||
!!! Tip "Explicit log configuration"
|
||||
We recommend to extract the logging configuration from your main configuration, and provide it to your bot via [multiple configuration files](configuration.md#multiple-configuration-files) functionality. This will avoid unnecessary code duplication.
|
||||
|
||||
---
|
||||
|
||||
On many Linux systems the bot can be configured to send its log messages to `syslog` or `journald` system services. Logging to a remote `syslog` server is also available on Windows. The special values for the `--logfile` command line option can be used for this.
|
||||
|
||||
### Logging to syslog
|
||||
|
||||
To send Freqtrade log messages to a local or remote `syslog` service use the `--logfile` command line option with the value in the following format:
|
||||
To send Freqtrade log messages to a local or remote `syslog` service use the `"log_config"` setup option to configure logging.
|
||||
|
||||
* `--logfile syslog:<syslog_address>` -- send log messages to `syslog` service using the `<syslog_address>` as the syslog address.
|
||||
``` json
|
||||
{
|
||||
// ...
|
||||
"log_config": {
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"syslog_fmt": {
|
||||
"format": "%(name)s - %(levelname)s - %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
// Other handlers?
|
||||
"syslog": {
|
||||
"class": "logging.handlers.SysLogHandler",
|
||||
"formatter": "syslog_fmt",
|
||||
// Use one of the other options above as address instead?
|
||||
"address": "/dev/log"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"handlers": [
|
||||
// other handlers
|
||||
"syslog",
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
So, the following are the examples of possible usages:
|
||||
[Additional log-handlers](#advanced-logging) may need to be configured to for example also have log output in the console.
|
||||
|
||||
* `--logfile syslog:/dev/log` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
|
||||
* `--logfile syslog` -- same as above, the shortcut for `/dev/log`.
|
||||
* `--logfile syslog:/var/run/syslog` -- log to syslog (rsyslog) using the `/var/run/syslog` socket. Use this on MacOS.
|
||||
* `--logfile syslog:localhost:514` -- log to local syslog using UDP socket, if it listens on port 514.
|
||||
* `--logfile syslog:<ip>:514` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
|
||||
#### Syslog usage
|
||||
|
||||
Log messages are send to `syslog` with the `user` facility. So you can see them with the following commands:
|
||||
|
||||
* `tail -f /var/log/user`, or
|
||||
* `tail -f /var/log/user`, or
|
||||
* install a comprehensive graphical viewer (for instance, 'Log File Viewer' for Ubuntu).
|
||||
|
||||
On many systems `syslog` (`rsyslog`) fetches data from `journald` (and vice versa), so both `--logfile syslog` or `--logfile journald` can be used and the messages be viewed with both `journalctl` and a syslog viewer utility. You can combine this in any way which suites you better.
|
||||
On many systems `syslog` (`rsyslog`) fetches data from `journald` (and vice versa), so both syslog or journald can be used and the messages be viewed with both `journalctl` and a syslog viewer utility. You can combine this in any way which suites you better.
|
||||
|
||||
For `rsyslog` the messages from the bot can be redirected into a separate dedicated log file. To achieve this, add
|
||||
|
||||
@@ -228,13 +309,69 @@ For `syslog` (`rsyslog`), the reduction mode can be switched on. This will reduc
|
||||
$RepeatedMsgReduction on
|
||||
```
|
||||
|
||||
#### Syslog addressing
|
||||
|
||||
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
|
||||
|
||||
|
||||
So, the following are the examples of possible addresses:
|
||||
|
||||
* `"address": "/dev/log"` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
|
||||
* `"address": "/var/run/syslog"` -- log to syslog (rsyslog) using the `/var/run/syslog` socket. Use this on MacOS.
|
||||
* `"address": "localhost:514"` -- log to local syslog using UDP socket, if it listens on port 514.
|
||||
* `"address": "<ip>:514"` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
|
||||
|
||||
|
||||
??? Info "Deprecated - configure syslog via command line"
|
||||
|
||||
`--logfile syslog:<syslog_address>` -- send log messages to `syslog` service using the `<syslog_address>` as the syslog address.
|
||||
|
||||
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
|
||||
|
||||
So, the following are the examples of possible usages:
|
||||
|
||||
* `--logfile syslog:/dev/log` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
|
||||
* `--logfile syslog` -- same as above, the shortcut for `/dev/log`.
|
||||
* `--logfile syslog:/var/run/syslog` -- log to syslog (rsyslog) using the `/var/run/syslog` socket. Use this on MacOS.
|
||||
* `--logfile syslog:localhost:514` -- log to local syslog using UDP socket, if it listens on port 514.
|
||||
* `--logfile syslog:<ip>:514` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
|
||||
|
||||
### Logging to journald
|
||||
|
||||
This needs the `cysystemd` python package installed as dependency (`pip install cysystemd`), which is not available on Windows. Hence, the whole journald logging functionality is not available for a bot running on Windows.
|
||||
|
||||
To send Freqtrade log messages to `journald` system service use the `--logfile` command line option with the value in the following format:
|
||||
To send Freqtrade log messages to `journald` system service, add the following configuration snippet to your configuration.
|
||||
|
||||
* `--logfile journald` -- send log messages to `journald`.
|
||||
``` json
|
||||
{
|
||||
// ...
|
||||
"log_config": {
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"journald_fmt": {
|
||||
"format": "%(name)s - %(levelname)s - %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
// Other handlers?
|
||||
"journald": {
|
||||
"class": "cysystemd.journal.JournaldLogHandler",
|
||||
"formatter": "journald_fmt",
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"handlers": [
|
||||
// ..
|
||||
"journald",
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[Additional log-handlers](#advanced-logging) may need to be configured to for example also have log output in the console.
|
||||
|
||||
Log messages are send to `journald` with the `user` facility. So you can see them with the following commands:
|
||||
|
||||
@@ -244,3 +381,51 @@ Log messages are send to `journald` with the `user` facility. So you can see the
|
||||
There are many other options in the `journalctl` utility to filter the messages, see manual pages for this utility.
|
||||
|
||||
On many systems `syslog` (`rsyslog`) fetches data from `journald` (and vice versa), so both `--logfile syslog` or `--logfile journald` can be used and the messages be viewed with both `journalctl` and a syslog viewer utility. You can combine this in any way which suites you better.
|
||||
|
||||
??? Info "Deprecated - configure journald via command line"
|
||||
To send Freqtrade log messages to `journald` system service use the `--logfile` command line option with the value in the following format:
|
||||
|
||||
`--logfile journald` -- send log messages to `journald`.
|
||||
|
||||
### Log format as JSON
|
||||
|
||||
You can also configure the default output stream to use JSON format instead.
|
||||
The "fmt_dict" attribute defines the keys for the json output - as well as the [python logging LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes).
|
||||
|
||||
The below configuration will change the default output to JSON. The same formatter could however also be used in combination with the `RotatingFileHandler`.
|
||||
We recommend to keep one format in human readable form.
|
||||
|
||||
``` json
|
||||
{
|
||||
// ...
|
||||
"log_config": {
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"json": {
|
||||
"()": "freqtrade.loggers.json_formatter.JsonFormatter",
|
||||
"fmt_dict": {
|
||||
"timestamp": "asctime",
|
||||
"level": "levelname",
|
||||
"logger": "name",
|
||||
"message": "message"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
// Other handlers?
|
||||
"jsonStream": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "json"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"handlers": [
|
||||
// ..
|
||||
"jsonStream",
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -209,6 +209,7 @@ A backtesting result will look like that:
|
||||
| Sortino | 1.88 |
|
||||
| Sharpe | 2.97 |
|
||||
| Calmar | 6.29 |
|
||||
| SQN | 2.45 |
|
||||
| Profit factor | 1.11 |
|
||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
@@ -315,6 +316,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
| Sortino | 1.88 |
|
||||
| Sharpe | 2.97 |
|
||||
| Calmar | 6.29 |
|
||||
| SQN | 2.45 |
|
||||
| Profit factor | 1.11 |
|
||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
@@ -368,6 +370,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
- `Sortino`: Annualized Sortino ratio.
|
||||
- `Sharpe`: Annualized Sharpe ratio.
|
||||
- `Calmar`: Annualized Calmar ratio.
|
||||
- `SQN`: System Quality Number (SQN) - by Van Tharp.
|
||||
- `Profit factor`: profit / loss.
|
||||
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
||||
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||
@@ -432,6 +435,20 @@ To save time, by default backtest will reuse a cached result from within the las
|
||||
To further analyze your backtest results, freqtrade will export the trades to file by default.
|
||||
You can then load the trades to perform further analysis as shown in the [data analysis](strategy_analysis_example.md#load-backtest-results-to-pandas-dataframe) backtesting section.
|
||||
|
||||
### Backtest output file
|
||||
|
||||
The output file freqtrade produces is a zip file containing the following files:
|
||||
|
||||
- The backtest report in json format
|
||||
- the market change data in feather format
|
||||
- a copy of the strategy file
|
||||
- a copy of the strategy parameters (if a parameter file was used)
|
||||
- a sanitized copy of the config file
|
||||
|
||||
This will ensure results are reproducible - under the assumption that the same data is available.
|
||||
|
||||
Only the strategy file and the config file are included in the zip file, eventual dependencies are not included.
|
||||
|
||||
## Assumptions made by backtesting
|
||||
|
||||
Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions:
|
||||
|
||||
+7
-3
@@ -54,11 +54,13 @@ By default, the bot loop runs every few seconds (`internals.process_throttle_sec
|
||||
* Check timeouts for open orders.
|
||||
* Calls `check_entry_timeout()` strategy callback for open entry orders.
|
||||
* Calls `check_exit_timeout()` strategy callback for open exit orders.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
||||
* Calls `adjust_order_price()` strategy callback for open orders.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders. *only called when `adjust_order_price()` is not implemented*
|
||||
* Calls `adjust_exit_price()` strategy callback for open exit orders. *only called when `adjust_order_price()` is not implemented*
|
||||
* Verifies existing positions and eventually places exit orders.
|
||||
* Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`.
|
||||
* Determine exit-price based on `exit_pricing` configuration setting or by using the `custom_exit_price()` callback.
|
||||
* Before a exit order is placed, `confirm_trade_exit()` strategy callback is called.
|
||||
* Before an exit order is placed, `confirm_trade_exit()` strategy callback is called.
|
||||
* Check position adjustments for open trades if enabled by calling `adjust_trade_position()` and place additional order if required.
|
||||
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
||||
* Verifies entry signal trying to enter new positions.
|
||||
@@ -80,7 +82,9 @@ This loop will be repeated again and again until the bot is stopped.
|
||||
* Loops per candle simulating entry and exit points.
|
||||
* Calls `bot_loop_start()` strategy callback.
|
||||
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
||||
* Calls `adjust_order_price()` strategy callback for open orders.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders. *only called when `adjust_order_price()` is not implemented!*
|
||||
* Calls `adjust_exit_price()` strategy callback for open exit orders. *only called when `adjust_order_price()` is not implemented!*
|
||||
* Check for trade entry signals (`enter_long` / `enter_short` columns).
|
||||
* Confirm trade entry / exits (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
||||
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
usage: freqtrade backtesting-show [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[-c PATH] [-d PATH] [--userdir PATH]
|
||||
[--export-filename PATH] [--show-pair-list]
|
||||
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -11,8 +11,9 @@ options:
|
||||
`--export` to be set as well. Example: `--export-filen
|
||||
ame=user_data/backtest_results/backtest_today.json`
|
||||
--show-pair-list Show backtesting pairlist sorted by profit.
|
||||
--breakdown {day,week,month} [{day,week,month} ...]
|
||||
Show backtesting breakdown per [day, week, month].
|
||||
--breakdown {day,week,month,year} [{day,week,month,year} ...]
|
||||
Show backtesting breakdown per [day, week, month,
|
||||
year].
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
||||
@@ -15,7 +15,7 @@ usage: freqtrade backtesting [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||
[--export {none,trades,signals}]
|
||||
[--export-filename PATH]
|
||||
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
||||
[--cache {none,day,week,month}]
|
||||
[--freqai-backtest-live-models]
|
||||
|
||||
@@ -65,8 +65,9 @@ options:
|
||||
Use this filename for backtest results.Requires
|
||||
`--export` to be set as well. Example: `--export-filen
|
||||
ame=user_data/backtest_results/backtest_today.json`
|
||||
--breakdown {day,week,month} [{day,week,month} ...]
|
||||
Show backtesting breakdown per [day, week, month].
|
||||
--breakdown {day,week,month,year} [{day,week,month,year} ...]
|
||||
Show backtesting breakdown per [day, week, month,
|
||||
year].
|
||||
--cache {none,day,week,month}
|
||||
Load a cached backtest result no older than specified
|
||||
age (default: day).
|
||||
|
||||
@@ -4,7 +4,7 @@ usage: freqtrade hyperopt-show [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[--profitable] [-n INT] [--print-json]
|
||||
[--hyperopt-filename FILENAME] [--no-header]
|
||||
[--disable-param-export]
|
||||
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -18,8 +18,9 @@ options:
|
||||
--no-header Do not print epoch details header.
|
||||
--disable-param-export
|
||||
Disable automatic hyperopt parameter export.
|
||||
--breakdown {day,week,month} [{day,week,month} ...]
|
||||
Show backtesting breakdown per [day, week, month].
|
||||
--breakdown {day,week,month,year} [{day,week,month,year} ...]
|
||||
Show backtesting breakdown per [day, week, month,
|
||||
year].
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
||||
@@ -282,6 +282,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
||||
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
||||
| `reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage (and decreasing train/inference timing in FreqAI). (Currently only affects FreqAI use-cases) <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
| `log_config` | Dictionary containing the log config for python logging. [more info](advanced-setup.md#advanced-logging) <br> **Datatype:** dict. <br> Default: `FtRichHandler`
|
||||
|
||||
### Parameters in the strategy
|
||||
|
||||
|
||||
@@ -88,3 +88,8 @@ Setting protections from the configuration via `"protections": [],` has been rem
|
||||
Using hdf5 as data storage has been deprecated in 2024.12 and was removed in 2025.1. We recommend switching to the feather data format.
|
||||
|
||||
Please use the [`convert-data` subcommand](data-download.md#sub-command-convert-data) to convert your existing data to one of the supported formats before updating.
|
||||
|
||||
## Configuring advanced logging via config
|
||||
|
||||
Configuring syslog and journald via `--logfile systemd` and `--logfile journald` respectively has been deprecated in 2025.3.
|
||||
Please use configuration based [log setup](advanced-setup.md#advanced-logging) instead.
|
||||
|
||||
@@ -44,9 +44,24 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
|
||||
|
||||
By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. The pairlist also supports wildcards (in regex-style) - so `.*/BTC` will include all pairs with BTC as a stake.
|
||||
|
||||
It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
|
||||
It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`, which in the below example, will trade BTC/USDT and ETH/USDT - and will prevent BNB/USDT trading.
|
||||
|
||||
Both `pair_*list` parameters support regex - so values like `.*/USDT` would enable trading all pairs that are not in the blacklist.
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "...",
|
||||
// ...
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT",
|
||||
"ETH/USDT",
|
||||
// ...
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/USDT",
|
||||
// ...
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList"}
|
||||
],
|
||||
@@ -377,6 +392,9 @@ If an incorrect category string is chosen, the plugin will print the available c
|
||||
!!! Warning "Many categories"
|
||||
Each added category corresponds to one API call to CoinGecko. The more categories you add, the longer the pairlist generation will take, potentially causing rate limit issues.
|
||||
|
||||
!!! Danger "Duplicate symbols in coingecko"
|
||||
Coingecko often has duplicate symbols, where the same symbol is used for different coins. Freqtrade will use the symbol as is and try to search for it on the exchange. If the symbol exists - it will be used. Freqtrade will however not check if the _intended_ symbol is the one coingecko meant. This can sometimes lead to unexpected results, especially on low volume coins or with meme coin categories.
|
||||
|
||||
#### AgeFilter
|
||||
|
||||
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||

|
||||
|
||||
[](https://github.com/freqtrade/freqtrade/actions/)
|
||||
[](https://github.com/freqtrade/freqtrade/actions/)
|
||||
[](https://doi.org/10.21105/joss.04864)
|
||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
markdown==3.7
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.5.50
|
||||
mkdocs-material==9.6.9
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.14.1
|
||||
jinja2==3.1.5
|
||||
pymdown-extensions==10.14.3
|
||||
jinja2==3.1.6
|
||||
mike==2.1.3
|
||||
|
||||
@@ -302,6 +302,19 @@ trades
|
||||
:param limit: Limits trades to the X last trades. Max 500 trades.
|
||||
:param offset: Offset by this amount of trades.
|
||||
|
||||
list_open_trades_custom_data
|
||||
Return a dict containing open trades custom-datas
|
||||
|
||||
:param key: str, optional - Key of the custom-data
|
||||
:param limit: Limits trades to X trades.
|
||||
:param offset: Offset by this amount of trades.
|
||||
|
||||
list_custom_data
|
||||
Return a dict containing custom-datas of a specified trade
|
||||
|
||||
:param trade_id: int - ID of the trade
|
||||
:param key: str, optional - Key of the custom-data
|
||||
|
||||
version
|
||||
Return the version of the bot.
|
||||
|
||||
|
||||
+6
-4
@@ -154,10 +154,10 @@ For example, simplified math:
|
||||
|
||||
In summary: The stoploss will be adjusted to be always be -10% of the highest observed price.
|
||||
|
||||
### Trailing stop loss, custom positive loss
|
||||
### Trailing stop loss, different positive loss
|
||||
|
||||
You could also have a default stop loss when you are in the red with your buy (buy - fee), but once you hit a positive result (or an offset you define) the system will utilize a new stop loss, which can have a different value.
|
||||
For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used.
|
||||
You could also have a default stop loss when you are in the red with your buy (buy - fee), but once you hit a positive result (or an offset you define) the system will utilize a new stop loss, with a different value.
|
||||
For example, your default stop loss is -10%, but once you have reached profitability (example 0.1%) a different trailing stoploss will be used.
|
||||
|
||||
!!! Note
|
||||
If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset).
|
||||
@@ -208,7 +208,9 @@ Before this, `stoploss` is used for the trailing stoploss.
|
||||
|
||||
You can also keep a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns.
|
||||
|
||||
If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`.
|
||||
If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss` and is not trailing.
|
||||
Leaving this value as `trailing_only_offset_is_reached=False` will allow the trailing stoploss to start trailing as soon as the asset price increases above the initial entry price.
|
||||
|
||||
This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset.
|
||||
|
||||
Configuration (offset is buy-price + 3%):
|
||||
|
||||
@@ -79,6 +79,8 @@ import talib.abstract as ta
|
||||
|
||||
class MyStrategy(IStrategy):
|
||||
|
||||
timeframe = '15m'
|
||||
|
||||
# set the initial stoploss to -10%
|
||||
stoploss = -0.10
|
||||
|
||||
|
||||
+56
-21
@@ -758,7 +758,7 @@ For performance reasons, it's disabled by default and freqtrade will show a warn
|
||||
|
||||
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
||||
|
||||
This callback is also called when there is an open order (either buy or sell) waiting for execution - and will cancel the existing open order to place a new order if the amount, price or direction is different.
|
||||
This callback is also called when there is an open order (either buy or sell) waiting for execution - and will cancel the existing open order to place a new order if the amount, price or direction is different. Also partially filled orders will be canceled, and will be replaced with the new amount as returned by the callback.
|
||||
|
||||
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
||||
|
||||
@@ -770,9 +770,10 @@ Modifications to leverage are not possible, and the stake-amount returned is ass
|
||||
The combined stake currently allocated to the position is held in `trade.stake_amount`. Therefore `trade.stake_amount` will always be updated on every additional entry and partial exit made through `adjust_trade_position()`.
|
||||
|
||||
!!! Danger "Loose Logic"
|
||||
On dry and live run, this function will be called every `throttle_process_secs` (default to 5s). If you have a loose logic, for example your logic for extra entry is only to check RSI of last candle is below 30, then when such condition fulfilled, your bot will do extra re-entry every 5 secs until either it run out of money, it hit the `max_position_adjustment` limit, or a new candle with RSI more than 30 arrived.
|
||||
On dry and live run, this function will be called every `throttle_process_secs` (default to 5s). If you have a loose logic, (e.g. increase position if RSI of the last candle is below 30), your bot will do extra re-entry every 5 secs until you either it run out of money, hit the `max_position_adjustment` limit, or a new candle with RSI more than 30 arrived.
|
||||
|
||||
Same thing also can happen with partial exit. So be sure to have a strict logic and/or check for the last filled order.
|
||||
Same thing also can happen with partial exit.
|
||||
So be sure to have a strict logic and/or check for the last filled order and if an order is already open.
|
||||
|
||||
!!! Warning "Performance with many position adjustments"
|
||||
Position adjustments can be a good approach to increase a strategy's output - but it can also have drawbacks if using this feature extensively.
|
||||
@@ -876,6 +877,9 @@ class DigDeeperStrategy(IStrategy):
|
||||
Return None for no action.
|
||||
Optionally, return a tuple with a 2nd element with an order reason
|
||||
"""
|
||||
if trade.has_open_orders:
|
||||
# Only act if no orders are open
|
||||
return
|
||||
|
||||
if current_profit > 0.05 and trade.nr_of_successful_exits == 0:
|
||||
# Take half of the profit at +5%
|
||||
@@ -934,28 +938,25 @@ class DigDeeperStrategy(IStrategy):
|
||||
|
||||
The total profit for this trade was 950$ on a 3350$ investment (`100@8$ + 100@9$ + 150@11$`). As such - the final relative profit is 28.35% (`950 / 3350`).
|
||||
|
||||
## Adjust Entry Price
|
||||
## Adjust order Price
|
||||
|
||||
The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles.
|
||||
The `adjust_order_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles.
|
||||
This callback is called once every iteration unless the order has been (re)placed within the current candle - limiting the maximum (re)placement of each order to once per candle.
|
||||
This also means that the first call will be at the start of the next candle after the initial order was placed.
|
||||
|
||||
Be aware that `custom_entry_price()` is still the one dictating initial entry limit order price target at the time of entry trigger.
|
||||
Be aware that `custom_entry_price()`/`custom_exit_price()` is still the one dictating initial limit order price target at the time of the signal.
|
||||
|
||||
Orders can be cancelled out of this callback by returning `None`.
|
||||
|
||||
Returning `current_order_rate` will keep the order on the exchange "as is".
|
||||
Returning any other price will cancel the existing order, and replace it with a new order.
|
||||
|
||||
The trade open-date (`trade.open_date_utc`) will remain at the time of the very first order placed.
|
||||
Please make sure to be aware of this - and eventually adjust your logic in other callbacks to account for this, and use the date of the first filled order instead.
|
||||
|
||||
If the cancellation of the original order fails, then the order will not be replaced - though the order will most likely have been canceled on exchange. Having this happen on initial entries will result in the deletion of the order, while on position adjustment orders, it'll result in the trade size remaining as is.
|
||||
If the order has been partially filled, the order will not be replaced. You can however use [`adjust_trade_position()`](#adjust-trade-position) to adjust the trade size to the full, expected position size, should this be necessary / desired.
|
||||
If the order has been partially filled, the order will not be replaced. You can however use [`adjust_trade_position()`](#adjust-trade-position) to adjust the trade size to the expected position size, should this be necessary / desired.
|
||||
|
||||
!!! Warning "Regular timeout"
|
||||
Entry `unfilledtimeout` mechanism (as well as `check_entry_timeout()`) takes precedence over this.
|
||||
Entry Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations.
|
||||
Entry `unfilledtimeout` mechanism (as well as `check_entry_timeout()`/`check_exit_timeout()`) takes precedence over this callback.
|
||||
Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations.
|
||||
|
||||
```python
|
||||
# Default imports
|
||||
@@ -964,14 +965,26 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def adjust_entry_price(self, trade: Trade, order: Order | None, pair: str,
|
||||
current_time: datetime, proposed_rate: float, current_order_rate: float,
|
||||
entry_tag: str | None, side: str, **kwargs) -> float:
|
||||
def adjust_order_price(
|
||||
self,
|
||||
trade: Trade,
|
||||
order: Order | None,
|
||||
pair: str,
|
||||
current_time: datetime,
|
||||
proposed_rate: float,
|
||||
current_order_rate: float,
|
||||
entry_tag: str | None,
|
||||
side: str,
|
||||
is_entry: bool,
|
||||
**kwargs,
|
||||
) -> float | None:
|
||||
"""
|
||||
Entry price re-adjustment logic, returning the user desired limit price.
|
||||
Exit and entry order price re-adjustment logic, returning the user desired limit price.
|
||||
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/
|
||||
|
||||
When not implemented by a strategy, returns current_order_rate as default.
|
||||
If current_order_rate is returned then the existing order is maintained.
|
||||
If None is returned then order gets canceled but not replaced by a new one.
|
||||
@@ -983,14 +996,16 @@ class AwesomeStrategy(IStrategy):
|
||||
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
|
||||
:param current_order_rate: Rate of the existing order in place.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: "long" or "short" - indicating the direction of the proposed trade
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param is_entry: True if the order is an entry order, False if it's an exit order.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New entry price value if provided
|
||||
|
||||
:return float or None: New entry price value if provided
|
||||
"""
|
||||
# Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
|
||||
|
||||
# Limit entry orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
|
||||
if (
|
||||
pair == "BTC/USDT"
|
||||
is_entry
|
||||
and pair == "BTC/USDT"
|
||||
and entry_tag == "long_sma200"
|
||||
and side == "long"
|
||||
and (current_time - timedelta(minutes=10)) <= trade.open_date_utc
|
||||
@@ -1007,6 +1022,26 @@ class AwesomeStrategy(IStrategy):
|
||||
return current_order_rate
|
||||
```
|
||||
|
||||
!!! danger "Incompatibility with `adjust_*_price()`"
|
||||
If you have both `adjust_order_price()` and `adjust_entry_price()`/`adjust_exit_price()` implemented, only `adjust_order_price()` will be used.
|
||||
If you need to adjust entry/exit prices, you can either implement the logic in `adjust_order_price()`, or use the split `adjust_entry_price()` / `adjust_exit_price()` callbacks, but not both.
|
||||
Mixing these is not supported and will raise an error during bot startup.
|
||||
|
||||
### Adjust Entry Price
|
||||
|
||||
The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace entry limit orders upon arrival.
|
||||
It's a sub-set of `adjust_order_price()` and is called only for entry orders.
|
||||
All remaining behavior is identical to `adjust_order_price()`.
|
||||
|
||||
The trade open-date (`trade.open_date_utc`) will remain at the time of the very first order placed.
|
||||
Please make sure to be aware of this - and eventually adjust your logic in other callbacks to account for this, and use the date of the first filled order instead.
|
||||
|
||||
### Adjust Exit Price
|
||||
|
||||
The `adjust_exit_price()` callback may be used by strategy developer to refresh/replace exit limit orders upon arrival.
|
||||
It's a sub-set of `adjust_order_price()` and is called only for exit orders.
|
||||
All remaining behavior is identical to `adjust_order_price()`.
|
||||
|
||||
## Leverage Callback
|
||||
|
||||
When trading in markets that allow leverage, this method must return the desired Leverage (Defaults to 1 -> No leverage).
|
||||
|
||||
@@ -513,7 +513,7 @@ By default, freqtrade will attempt to load strategies from all `.py` files withi
|
||||
Assuming your strategy is called `AwesomeStrategy`, stored in the file `user_data/strategies/AwesomeStrategy.py`, then you can start freqtrade in dry (or live, depending on your configuration) mode with:
|
||||
|
||||
```bash
|
||||
freqtrade trade --strategy AwesomeStrategy`
|
||||
freqtrade trade --strategy AwesomeStrategy
|
||||
```
|
||||
|
||||
Note that we're using the class name, not the file name.
|
||||
@@ -1122,6 +1122,7 @@ The following list contains some common patterns which should be avoided to prev
|
||||
- don't use `.iloc[-1]` or any other absolute position in the dataframe within `populate_` functions, as this will be different between dry-run and backtesting. Absolute `iloc` indexing is safe to use in callbacks however - see [Strategy Callbacks](strategy-callbacks.md).
|
||||
- don't use functions that use all dataframe or column values, e.g. `dataframe['mean_volume'] = dataframe['volume'].mean()`. As backtesting uses the full dataframe, at any point in the dataframe, the `'mean_volume'` series would include data from the future. Use rolling() calculations instead, e.g. `dataframe['volume'].rolling(<window>).mean()`.
|
||||
- don't use `.resample('1h')`. This uses the left border of the period interval, so moves data from an hour boundary to the start of the hour. Use `.resample('1h', label='right')` instead.
|
||||
- don't use `.merge()` to combine longer timeframes onto shorter ones. Instead, use the [informative pair](#informative-pairs) helpers. (A plain merge can implicitly cause a lookahead bias as date refers to open date, not close date).
|
||||
|
||||
!!! Tip "Identifying problems"
|
||||
You should always use the two helper commands [lookahead-analysis](lookahead-analysis.md) and [recursive-analysis](recursive-analysis.md), which can each help you figure out problems with your strategy in different ways.
|
||||
|
||||
@@ -81,6 +81,19 @@ Without this, the bot will always respond to the general channel in the group if
|
||||
|
||||
Similar to the group-id - you can use `/tg_info` from the topic/thread to get the correct topic-id.
|
||||
|
||||
#### Authorized users
|
||||
|
||||
For groups, it can be useful to limit who can send commands to the bot.
|
||||
|
||||
If `"authorized_users": []` is present and empty, no user will be allowed to control the bot.
|
||||
In the below example, only the user with the id "1234567" is allowed to control the bot - all other users will only be able to receive messages.
|
||||
|
||||
```json
|
||||
"chat_id": "-1001332619709",
|
||||
"topic_id": "3",
|
||||
"authorized_users": ["1234567"]
|
||||
```
|
||||
|
||||
## Control telegram noise
|
||||
|
||||
Freqtrade provides means to control the verbosity of your telegram bot.
|
||||
|
||||
@@ -35,6 +35,7 @@ The following attributes / properties are available for each individual trade -
|
||||
| `trade_direction` | "long" / "short" | Trade direction in text - long or short. |
|
||||
| `nr_of_successful_entries` | int | Number of successful (filled) entry orders. |
|
||||
| `nr_of_successful_exits` | int | Number of successful (filled) exit orders. |
|
||||
| `has_open_orders` | boolean | Has the trade open orders (excluding stoploss orders). |
|
||||
|
||||
## Class methods
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2025.1"
|
||||
__version__ = "2025.3"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -224,7 +224,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"backtest_breakdown": Arg(
|
||||
"--breakdown",
|
||||
help="Show backtesting breakdown per [day, week, month].",
|
||||
help="Show backtesting breakdown per [day, week, month, year].",
|
||||
nargs="+",
|
||||
choices=constants.BACKTEST_BREAKDOWNS,
|
||||
),
|
||||
|
||||
@@ -17,11 +17,11 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from freqtrade.exchange import list_available_exchanges
|
||||
from freqtrade.loggers.rich_console import get_rich_console
|
||||
|
||||
available_exchanges: list[ValidExchangesType] = list_available_exchanges(
|
||||
args["list_exchanges_all"]
|
||||
@@ -77,15 +77,16 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
||||
)
|
||||
# table.add_row(*[exchange[header] for header in headers])
|
||||
|
||||
console = Console()
|
||||
console = get_rich_console()
|
||||
console.print(table)
|
||||
|
||||
|
||||
def _print_objs_tabular(objs: list, print_colorized: bool) -> None:
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from freqtrade.loggers.rich_console import get_rich_console
|
||||
|
||||
names = [s["name"] for s in objs]
|
||||
objs_to_print: list[dict[str, Text | str]] = [
|
||||
{
|
||||
@@ -118,10 +119,7 @@ def _print_objs_tabular(objs: list, print_colorized: bool) -> None:
|
||||
for row in objs_to_print:
|
||||
table.add_row(*[row[header] for header in objs_to_print[0].keys()])
|
||||
|
||||
console = Console(
|
||||
color_system="auto" if print_colorized else None,
|
||||
width=200 if "pytest" in sys.modules else None,
|
||||
)
|
||||
console = get_rich_console(color_system="auto" if print_colorized else None)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@@ -219,7 +217,7 @@ def start_list_markets(args: dict[str, Any], pairs_only: bool = False) -> None:
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.exchange import market_is_active
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.misc import plural, safe_value_fallback
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.util import print_rich_table
|
||||
|
||||
@@ -246,88 +244,99 @@ def start_list_markets(args: dict[str, Any], pairs_only: bool = False) -> None:
|
||||
except Exception as e:
|
||||
raise OperationalException(f"Cannot get markets. Reason: {e}") from e
|
||||
|
||||
else:
|
||||
summary_str = (
|
||||
(f"Exchange {exchange.name} has {len(pairs)} ")
|
||||
+ ("active " if active_only else "")
|
||||
+ (plural(len(pairs), "pair" if pairs_only else "market"))
|
||||
+ (
|
||||
f" with {', '.join(base_currencies)} as base "
|
||||
f"{plural(len(base_currencies), 'currency', 'currencies')}"
|
||||
if base_currencies
|
||||
else ""
|
||||
)
|
||||
+ (" and" if base_currencies and quote_currencies else "")
|
||||
+ (
|
||||
f" with {', '.join(quote_currencies)} as quote "
|
||||
f"{plural(len(quote_currencies), 'currency', 'currencies')}"
|
||||
if quote_currencies
|
||||
else ""
|
||||
)
|
||||
tickers = exchange.get_tickers()
|
||||
|
||||
summary_str = (
|
||||
(f"Exchange {exchange.name} has {len(pairs)} ")
|
||||
+ ("active " if active_only else "")
|
||||
+ (plural(len(pairs), "pair" if pairs_only else "market"))
|
||||
+ (
|
||||
f" with {', '.join(base_currencies)} as base "
|
||||
f"{plural(len(base_currencies), 'currency', 'currencies')}"
|
||||
if base_currencies
|
||||
else ""
|
||||
)
|
||||
+ (" and" if base_currencies and quote_currencies else "")
|
||||
+ (
|
||||
f" with {', '.join(quote_currencies)} as quote "
|
||||
f"{plural(len(quote_currencies), 'currency', 'currencies')}"
|
||||
if quote_currencies
|
||||
else ""
|
||||
)
|
||||
)
|
||||
|
||||
headers = [
|
||||
"Id",
|
||||
"Symbol",
|
||||
"Base",
|
||||
"Quote",
|
||||
"Active",
|
||||
"Spot",
|
||||
"Margin",
|
||||
"Future",
|
||||
"Leverage",
|
||||
]
|
||||
headers = [
|
||||
"Id",
|
||||
"Symbol",
|
||||
"Base",
|
||||
"Quote",
|
||||
"Active",
|
||||
"Spot",
|
||||
"Margin",
|
||||
"Future",
|
||||
"Leverage",
|
||||
"Min Stake",
|
||||
]
|
||||
|
||||
tabular_data = [
|
||||
{
|
||||
"Id": v["id"],
|
||||
"Symbol": v["symbol"],
|
||||
"Base": v["base"],
|
||||
"Quote": v["quote"],
|
||||
"Active": market_is_active(v),
|
||||
"Spot": "Spot" if exchange.market_is_spot(v) else "",
|
||||
"Margin": "Margin" if exchange.market_is_margin(v) else "",
|
||||
"Future": "Future" if exchange.market_is_future(v) else "",
|
||||
"Leverage": exchange.get_max_leverage(v["symbol"], 20),
|
||||
}
|
||||
for _, v in pairs.items()
|
||||
]
|
||||
tabular_data = [
|
||||
{
|
||||
"Id": v["id"],
|
||||
"Symbol": v["symbol"],
|
||||
"Base": v["base"],
|
||||
"Quote": v["quote"],
|
||||
"Active": market_is_active(v),
|
||||
"Spot": "Spot" if exchange.market_is_spot(v) else "",
|
||||
"Margin": "Margin" if exchange.market_is_margin(v) else "",
|
||||
"Future": "Future" if exchange.market_is_future(v) else "",
|
||||
"Leverage": exchange.get_max_leverage(v["symbol"], 20),
|
||||
"Min Stake": round(
|
||||
exchange.get_min_pair_stake_amount(
|
||||
v["symbol"],
|
||||
safe_value_fallback(tickers.get(v["symbol"], {}), "last", "ask", 0.0),
|
||||
0.0,
|
||||
)
|
||||
or 0.0,
|
||||
8,
|
||||
),
|
||||
}
|
||||
for _, v in pairs.items()
|
||||
]
|
||||
|
||||
if (
|
||||
args.get("print_one_column", False)
|
||||
or args.get("list_pairs_print_json", False)
|
||||
or args.get("print_csv", False)
|
||||
):
|
||||
# Print summary string in the log in case of machine-readable
|
||||
# regular formats.
|
||||
logger.info(f"{summary_str}.")
|
||||
if (
|
||||
args.get("print_one_column", False)
|
||||
or args.get("list_pairs_print_json", False)
|
||||
or args.get("print_csv", False)
|
||||
):
|
||||
# Print summary string in the log in case of machine-readable
|
||||
# regular formats.
|
||||
logger.info(f"{summary_str}.")
|
||||
else:
|
||||
# Print empty string separating leading logs and output in case of
|
||||
# human-readable formats.
|
||||
print()
|
||||
|
||||
if pairs:
|
||||
if args.get("print_list", False):
|
||||
# print data as a list, with human-readable summary
|
||||
print(f"{summary_str}: {', '.join(pairs.keys())}.")
|
||||
elif args.get("print_one_column", False):
|
||||
print("\n".join(pairs.keys()))
|
||||
elif args.get("list_pairs_print_json", False):
|
||||
import rapidjson
|
||||
|
||||
print(rapidjson.dumps(list(pairs.keys()), default=str))
|
||||
elif args.get("print_csv", False):
|
||||
writer = csv.DictWriter(sys.stdout, fieldnames=headers)
|
||||
writer.writeheader()
|
||||
writer.writerows(tabular_data)
|
||||
else:
|
||||
# Print empty string separating leading logs and output in case of
|
||||
# human-readable formats.
|
||||
print()
|
||||
|
||||
if pairs:
|
||||
if args.get("print_list", False):
|
||||
# print data as a list, with human-readable summary
|
||||
print(f"{summary_str}: {', '.join(pairs.keys())}.")
|
||||
elif args.get("print_one_column", False):
|
||||
print("\n".join(pairs.keys()))
|
||||
elif args.get("list_pairs_print_json", False):
|
||||
import rapidjson
|
||||
|
||||
print(rapidjson.dumps(list(pairs.keys()), default=str))
|
||||
elif args.get("print_csv", False):
|
||||
writer = csv.DictWriter(sys.stdout, fieldnames=headers)
|
||||
writer.writeheader()
|
||||
writer.writerows(tabular_data)
|
||||
else:
|
||||
print_rich_table(tabular_data, headers, summary_str)
|
||||
elif not (
|
||||
args.get("print_one_column", False)
|
||||
or args.get("list_pairs_print_json", False)
|
||||
or args.get("print_csv", False)
|
||||
):
|
||||
print(f"{summary_str}.")
|
||||
print_rich_table(tabular_data, headers, summary_str)
|
||||
elif not (
|
||||
args.get("print_one_column", False)
|
||||
or args.get("list_pairs_print_json", False)
|
||||
or args.get("print_csv", False)
|
||||
):
|
||||
print(f"{summary_str}.")
|
||||
|
||||
|
||||
def start_show_trades(args: dict[str, Any]) -> None:
|
||||
|
||||
@@ -425,6 +425,10 @@ CONF_SCHEMA = {
|
||||
"description": "Edge configuration.",
|
||||
"$ref": "#/definitions/edge",
|
||||
},
|
||||
"log_config": {
|
||||
"description": "Logging configuration.",
|
||||
"$ref": "#/definitions/logging",
|
||||
},
|
||||
"freqai": {
|
||||
"description": "FreqAI configuration.",
|
||||
"$ref": "#/definitions/freqai",
|
||||
@@ -471,6 +475,12 @@ CONF_SCHEMA = {
|
||||
"description": "Telegram topic ID - only applicable for group chats",
|
||||
"type": "string",
|
||||
},
|
||||
"authorized_users": {
|
||||
"description": "Authorized users for the bot.",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": True,
|
||||
},
|
||||
"allow_custom_messages": {
|
||||
"description": "Allow sending custom messages from the Strategy.",
|
||||
"type": "boolean",
|
||||
@@ -877,6 +887,28 @@ CONF_SCHEMA = {
|
||||
},
|
||||
"required": ["process_throttle_secs", "allowed_risk"],
|
||||
},
|
||||
"logging": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {"type": "number", "const": 1},
|
||||
"formatters": {
|
||||
"type": "object",
|
||||
# In theory the below, but can be more flexible
|
||||
# based on logging.config documentation
|
||||
# "additionalProperties": {
|
||||
# "type": "object",
|
||||
# "properties": {
|
||||
# "format": {"type": "string"},
|
||||
# "datefmt": {"type": "string"},
|
||||
# },
|
||||
# "required": ["format"],
|
||||
# },
|
||||
},
|
||||
"handlers": {"type": "object"},
|
||||
"root": {"type": "object"},
|
||||
},
|
||||
"required": ["version", "formatters", "handlers", "root"],
|
||||
},
|
||||
"external_message_consumer": {
|
||||
"description": "Configuration for external message consumer.",
|
||||
"type": "object",
|
||||
@@ -965,10 +997,13 @@ CONF_SCHEMA = {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"keras": {
|
||||
"description": "Use Keras for model training.",
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"identifier": {
|
||||
"description": (
|
||||
"A unique ID for the current model. "
|
||||
"Must be changed when modifying features."
|
||||
),
|
||||
"type": "string",
|
||||
"default": "example",
|
||||
},
|
||||
"write_metrics_to_disk": {
|
||||
"description": "Write metrics to disk?",
|
||||
@@ -1000,13 +1035,42 @@ CONF_SCHEMA = {
|
||||
"type": "number",
|
||||
"default": 7,
|
||||
},
|
||||
"identifier": {
|
||||
"live_retrain_hours": {
|
||||
"description": "Frequency of retraining during dry/live runs.",
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
},
|
||||
"expiration_hours": {
|
||||
"description": (
|
||||
"A unique ID for the current model. "
|
||||
"Must be changed when modifying features."
|
||||
"Avoid making predictions if a model is more than `expiration_hours` "
|
||||
"old. Defaults to 0 (no expiration)."
|
||||
),
|
||||
"type": "string",
|
||||
"default": "example",
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
},
|
||||
"save_backtest_models": {
|
||||
"description": "Save models to disk when running backtesting.",
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"fit_live_predictions_candles": {
|
||||
"description": (
|
||||
"Number of historical candles to use for computing target (label) "
|
||||
"statistics from prediction data, instead of from the training dataset."
|
||||
),
|
||||
"type": "integer",
|
||||
},
|
||||
"data_kitchen_thread_count": {
|
||||
"description": (
|
||||
"Designate the number of threads you want to use for data processing "
|
||||
"(outlier methods, normalization, etc.)."
|
||||
),
|
||||
"type": "integer",
|
||||
},
|
||||
"activate_tensorboard": {
|
||||
"description": "Indicate whether or not to activate tensorboard",
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
},
|
||||
"wait_for_training_iteration_on_reload": {
|
||||
"description": (
|
||||
@@ -1015,6 +1079,20 @@ CONF_SCHEMA = {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
},
|
||||
"continual_learning": {
|
||||
"description": (
|
||||
"Use the final state of the most recently trained model "
|
||||
"as starting point for the new model, allowing for "
|
||||
"incremental learning."
|
||||
),
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"keras": {
|
||||
"description": "Use Keras for model training.",
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"feature_parameters": {
|
||||
"description": "The parameters used to engineer the feature set",
|
||||
"type": "object",
|
||||
|
||||
@@ -59,7 +59,7 @@ AVAILABLE_PAIRLISTS = [
|
||||
"VolatilityFilter",
|
||||
]
|
||||
AVAILABLE_DATAHANDLERS = ["json", "jsongz", "feather", "parquet"]
|
||||
BACKTEST_BREAKDOWNS = ["day", "week", "month"]
|
||||
BACKTEST_BREAKDOWNS = ["day", "week", "month", "year"]
|
||||
BACKTEST_CACHE_AGE = ["none", "day", "week", "month"]
|
||||
BACKTEST_CACHE_DEFAULT = "day"
|
||||
DRY_RUN_WALLET = 1000
|
||||
|
||||
@@ -49,7 +49,7 @@ class DataProvider:
|
||||
self._pairlists = pairlists
|
||||
self.__rpc = rpc
|
||||
self.__cached_pairs: dict[PairWithTimeframe, tuple[DataFrame, datetime]] = {}
|
||||
self.__slice_index: int | None = None
|
||||
self.__slice_index: dict[str, int] = {}
|
||||
self.__slice_date: datetime | None = None
|
||||
|
||||
self.__cached_pairs_backtesting: dict[PairWithTimeframe, DataFrame] = {}
|
||||
@@ -69,13 +69,13 @@ class DataProvider:
|
||||
self.producers = self._config.get("external_message_consumer", {}).get("producers", [])
|
||||
self.external_data_enabled = len(self.producers) > 0
|
||||
|
||||
def _set_dataframe_max_index(self, limit_index: int):
|
||||
def _set_dataframe_max_index(self, pair: str, limit_index: int):
|
||||
"""
|
||||
Limit analyzed dataframe to max specified index.
|
||||
Only relevant in backtesting.
|
||||
:param limit_index: dataframe index.
|
||||
"""
|
||||
self.__slice_index = limit_index
|
||||
self.__slice_index[pair] = limit_index
|
||||
|
||||
def _set_dataframe_max_date(self, limit_date: datetime):
|
||||
"""
|
||||
@@ -393,9 +393,10 @@ class DataProvider:
|
||||
df, date = self.__cached_pairs[pair_key]
|
||||
else:
|
||||
df, date = self.__cached_pairs[pair_key]
|
||||
if self.__slice_index is not None:
|
||||
max_index = self.__slice_index
|
||||
if (max_index := self.__slice_index.get(pair)) is not None:
|
||||
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES) : max_index]
|
||||
else:
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
||||
return df, date
|
||||
else:
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
||||
@@ -430,7 +431,7 @@ class DataProvider:
|
||||
# Don't reset backtesting pairs -
|
||||
# otherwise they're reloaded each time during hyperopt due to with analyze_per_epoch
|
||||
# self.__cached_pairs_backtesting = {}
|
||||
self.__slice_index = 0
|
||||
self.__slice_index = {}
|
||||
|
||||
# Exchange functions
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ from freqtrade.enums import CandleType, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||
from freqtrade.util import dt_now, dt_ts, format_ms_time
|
||||
from freqtrade.util.datetime_helpers import format_ms_time_det
|
||||
from freqtrade.util import dt_now, dt_ts, format_ms_time, format_ms_time_det
|
||||
from freqtrade.util.migrations import migrate_data
|
||||
from freqtrade.util.progress_tracker import CustomProgress, retrieve_progress_tracker
|
||||
|
||||
|
||||
@@ -375,3 +375,32 @@ def calculate_calmar(
|
||||
|
||||
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||
return calmar_ratio
|
||||
|
||||
|
||||
def calculate_sqn(trades: pd.DataFrame, starting_balance: float) -> float:
|
||||
"""
|
||||
Calculate System Quality Number (SQN) - Van K. Tharp.
|
||||
SQN measures systematic trading quality and takes into account both
|
||||
the number of trades and their standard deviation.
|
||||
|
||||
:param trades: DataFrame containing trades (requires column profit_abs)
|
||||
:param starting_balance: Starting balance of the trading system
|
||||
:return: SQN value
|
||||
"""
|
||||
if len(trades) == 0:
|
||||
return 0.0
|
||||
|
||||
total_profit = trades["profit_abs"] / starting_balance
|
||||
number_of_trades = len(trades)
|
||||
|
||||
# Calculate average trade and standard deviation
|
||||
average_profits = total_profit.mean()
|
||||
profits_std = total_profit.std()
|
||||
|
||||
if profits_std != 0 and not np.isnan(profits_std):
|
||||
sqn = math.sqrt(number_of_trades) * (average_profits / profits_std)
|
||||
else:
|
||||
# Define negative SQN to indicate this is NOT optimal
|
||||
sqn = -100.0
|
||||
|
||||
return round(sqn, 4)
|
||||
|
||||
@@ -10,7 +10,6 @@ from freqtrade.exchange.bitmart import Bitmart
|
||||
from freqtrade.exchange.bitpanda import Bitpanda
|
||||
from freqtrade.exchange.bitvavo import Bitvavo
|
||||
from freqtrade.exchange.bybit import Bybit
|
||||
from freqtrade.exchange.coinbasepro import Coinbasepro
|
||||
from freqtrade.exchange.cryptocom import Cryptocom
|
||||
from freqtrade.exchange.exchange_utils import (
|
||||
ROUND_DOWN,
|
||||
|
||||
@@ -11,7 +11,11 @@ from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.binance_public_data import concat_safe, download_archive_ohlcv
|
||||
from freqtrade.exchange.binance_public_data import (
|
||||
concat_safe,
|
||||
download_archive_ohlcv,
|
||||
download_archive_trades,
|
||||
)
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_types import FtHas, Tickers
|
||||
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
|
||||
@@ -29,7 +33,6 @@ class Binance(Exchange):
|
||||
"stop_price_prop": "stopPrice",
|
||||
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "fromId",
|
||||
"trades_has_history": True,
|
||||
@@ -37,6 +40,7 @@ class Binance(Exchange):
|
||||
"ws_enabled": True,
|
||||
}
|
||||
_ft_has_futures: FtHas = {
|
||||
"funding_fee_candle_limit": 1000,
|
||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC"],
|
||||
"tickers_have_price": False,
|
||||
@@ -140,9 +144,10 @@ class Binance(Exchange):
|
||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||
"""
|
||||
if is_new_pair:
|
||||
x = self.loop.run_until_complete(
|
||||
self._async_get_candle_history(pair, timeframe, candle_type, 0)
|
||||
)
|
||||
with self._loop_lock:
|
||||
x = self.loop.run_until_complete(
|
||||
self._async_get_candle_history(pair, timeframe, candle_type, 0)
|
||||
)
|
||||
if x and x[3] and x[3][0] and x[3][0][0] > since_ms:
|
||||
# Set starting date to first available candle.
|
||||
since_ms = x[3][0][0]
|
||||
@@ -201,16 +206,17 @@ class Binance(Exchange):
|
||||
"""
|
||||
Fastly fetch OHLCV data by leveraging https://data.binance.vision.
|
||||
"""
|
||||
df = self.loop.run_until_complete(
|
||||
download_archive_ohlcv(
|
||||
candle_type=candle_type,
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms,
|
||||
until_ms=until_ms,
|
||||
markets=self.markets,
|
||||
with self._loop_lock:
|
||||
df = self.loop.run_until_complete(
|
||||
download_archive_ohlcv(
|
||||
candle_type=candle_type,
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms,
|
||||
until_ms=until_ms,
|
||||
markets=self.markets,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# download the remaining data from rest API
|
||||
if df.empty:
|
||||
@@ -268,12 +274,12 @@ class Binance(Exchange):
|
||||
def dry_run_liquidation_price(
|
||||
self,
|
||||
pair: str,
|
||||
open_rate: float, # Entry price of position
|
||||
open_rate: float,
|
||||
is_short: bool,
|
||||
amount: float,
|
||||
stake_amount: float,
|
||||
leverage: float,
|
||||
wallet_balance: float, # Or margin balance
|
||||
wallet_balance: float,
|
||||
open_trades: list,
|
||||
) -> float | None:
|
||||
"""
|
||||
@@ -287,8 +293,6 @@ class Binance(Exchange):
|
||||
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||
:param stake_amount: Stake amount - Collateral in settle currency.
|
||||
:param leverage: Leverage used for this position.
|
||||
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
||||
:param margin_mode: Either ISOLATED or CROSS
|
||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||
Cross-Margin Mode: crossWalletBalance
|
||||
Isolated-Margin Mode: isolatedWalletBalance
|
||||
@@ -377,3 +381,48 @@ class Binance(Exchange):
|
||||
if not t:
|
||||
return [], "0"
|
||||
return t, from_id
|
||||
|
||||
async def _async_get_trade_history_id(
|
||||
self, pair: str, until: int, since: int, from_id: str | None = None
|
||||
) -> tuple[str, list[list]]:
|
||||
logger.info(f"Fetching trades from Binance, {from_id=}, {since=}, {until=}")
|
||||
|
||||
if not self._config["exchange"].get("only_from_ccxt", False):
|
||||
if from_id is None or not since:
|
||||
trades = await self._api_async.fetch_trades(
|
||||
pair,
|
||||
params={
|
||||
self._trades_pagination_arg: "0",
|
||||
},
|
||||
limit=5,
|
||||
)
|
||||
listing_date: int = trades[0]["timestamp"]
|
||||
since = max(since, listing_date)
|
||||
|
||||
_, res = await download_archive_trades(
|
||||
CandleType.SPOT,
|
||||
pair,
|
||||
since_ms=since,
|
||||
until_ms=until,
|
||||
markets=self.markets,
|
||||
)
|
||||
|
||||
if not res:
|
||||
end_time = since
|
||||
end_id = from_id
|
||||
else:
|
||||
end_time = res[-1][0]
|
||||
end_id = res[-1][1]
|
||||
|
||||
if end_time and end_time >= until:
|
||||
return pair, res
|
||||
else:
|
||||
_, res2 = await super()._async_get_trade_history_id(
|
||||
pair, until=until, since=end_time, from_id=end_id
|
||||
)
|
||||
res.extend(res2)
|
||||
return pair, res
|
||||
|
||||
return await super()._async_get_trade_history_id(
|
||||
pair, until=until, since=since, from_id=from_id
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
"""
|
||||
Fetch daily-archived OHLCV data from https://data.binance.vision/
|
||||
Documentation can be found in https://github.com/binance/binance-public-data
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -10,9 +11,11 @@ from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import DEFAULT_TRADES_COLUMNS
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.misc import chunks
|
||||
from freqtrade.util.datetime_helpers import dt_from_ts, dt_now
|
||||
@@ -157,8 +160,8 @@ async def _download_archive_ohlcv(
|
||||
return concat_safe(dfs)
|
||||
else:
|
||||
dfs.append(None)
|
||||
except BaseException as e:
|
||||
logger.warning(f"An exception raised: : {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"An exception raised: {e}")
|
||||
# Directly return the existing data, do not allow the gap within the data
|
||||
await cancel_and_await_tasks(tasks[tasks.index(task) + 1 :])
|
||||
return concat_safe(dfs)
|
||||
@@ -212,6 +215,20 @@ def binance_vision_ohlcv_zip_url(
|
||||
return url
|
||||
|
||||
|
||||
def binance_vision_trades_zip_url(symbol: str, candle_type: CandleType, date: date) -> str:
|
||||
"""
|
||||
example urls:
|
||||
https://data.binance.vision/data/spot/daily/aggTrades/BTCUSDT/BTCUSDT-aggTrades-2023-10-27.zip
|
||||
https://data.binance.vision/data/futures/um/daily/aggTrades/BTCUSDT/BTCUSDT-aggTrades-2023-10-27.zip
|
||||
"""
|
||||
asset_type_url_segment = candle_type_to_url_segment(candle_type)
|
||||
url = (
|
||||
f"https://data.binance.vision/data/{asset_type_url_segment}/daily/aggTrades/{symbol}"
|
||||
f"/{symbol}-aggTrades-{date.strftime('%Y-%m-%d')}.zip"
|
||||
)
|
||||
return url
|
||||
|
||||
|
||||
async def get_daily_ohlcv(
|
||||
symbol: str,
|
||||
timeframe: str,
|
||||
@@ -268,7 +285,11 @@ async def get_daily_ohlcv(
|
||||
names=["date", "open", "high", "low", "close", "volume"],
|
||||
header=header,
|
||||
)
|
||||
df["date"] = pd.to_datetime(df["date"], unit="ms", utc=True)
|
||||
df["date"] = pd.to_datetime(
|
||||
np.where(df["date"] > 1e13, df["date"] // 1000, df["date"]),
|
||||
unit="ms",
|
||||
utc=True,
|
||||
)
|
||||
return df
|
||||
elif resp.status == 404:
|
||||
logger.debug(f"Failed to download {url}")
|
||||
@@ -280,3 +301,203 @@ async def get_daily_ohlcv(
|
||||
if isinstance(e, Http404) or retry > retry_count:
|
||||
logger.debug(f"Failed to get data from {url}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def download_archive_trades(
|
||||
candle_type: CandleType,
|
||||
pair: str,
|
||||
*,
|
||||
since_ms: int,
|
||||
until_ms: int | None,
|
||||
markets: dict[str, Any],
|
||||
stop_on_404: bool = True,
|
||||
) -> tuple[str, list[list]]:
|
||||
try:
|
||||
symbol = markets[pair]["id"]
|
||||
|
||||
last_available_date = dt_now() - timedelta(days=2)
|
||||
|
||||
start = dt_from_ts(since_ms)
|
||||
end = dt_from_ts(until_ms) if until_ms else dt_now()
|
||||
end = min(end, last_available_date)
|
||||
if start >= end:
|
||||
return pair, []
|
||||
result_list = await _download_archive_trades(
|
||||
symbol, pair, candle_type, start, end, stop_on_404
|
||||
)
|
||||
return pair, result_list
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"An exception occurred during fast trades download from Binance, falling back to "
|
||||
"the slower REST API, this can take a lot more time.",
|
||||
exc_info=e,
|
||||
)
|
||||
return pair, []
|
||||
|
||||
|
||||
def parse_trades_from_zip(csvf):
|
||||
# https://github.com/binance/binance-public-data/issues/283
|
||||
first_byte = csvf.read(1)[0]
|
||||
if chr(first_byte).isdigit():
|
||||
# spot
|
||||
header = None
|
||||
names = [
|
||||
"id",
|
||||
"price",
|
||||
"amount",
|
||||
"first_trade_id",
|
||||
"last_trade_id",
|
||||
"timestamp",
|
||||
"is_buyer_maker",
|
||||
"is_best_match",
|
||||
]
|
||||
else:
|
||||
# futures
|
||||
header = 0
|
||||
names = [
|
||||
"id",
|
||||
"price",
|
||||
"amount",
|
||||
"first_trade_id",
|
||||
"last_trade_id",
|
||||
"timestamp",
|
||||
"is_buyer_maker",
|
||||
]
|
||||
csvf.seek(0)
|
||||
|
||||
df = pd.read_csv(
|
||||
csvf,
|
||||
names=names,
|
||||
header=header,
|
||||
)
|
||||
df.loc[:, "cost"] = df["price"] * df["amount"]
|
||||
# Side is reversed intentionally
|
||||
# based on ccxt parseTrade logic.
|
||||
df.loc[:, "side"] = np.where(df["is_buyer_maker"], "sell", "buy")
|
||||
df.loc[:, "type"] = None
|
||||
# Convert timestamp to ms
|
||||
df.loc[:, "timestamp"] = np.where(
|
||||
df["timestamp"] > 1e13,
|
||||
df["timestamp"] // 1000,
|
||||
df["timestamp"],
|
||||
)
|
||||
return df.loc[:, DEFAULT_TRADES_COLUMNS].to_records(index=False).tolist()
|
||||
|
||||
|
||||
async def get_daily_trades(
|
||||
symbol: str,
|
||||
candle_type: CandleType,
|
||||
date: date,
|
||||
session: aiohttp.ClientSession,
|
||||
retry_count: int = 3,
|
||||
retry_delay: float = 0.0,
|
||||
) -> list[list]:
|
||||
"""
|
||||
Get daily OHLCV from https://data.binance.vision
|
||||
See https://github.com/binance/binance-public-data
|
||||
|
||||
:symbol: binance symbol name, e.g. BTCUSDT
|
||||
:candle_type: SPOT or FUTURES
|
||||
:date: the returned DataFrame will cover the entire day of `date` in UTC
|
||||
:session: an aiohttp.ClientSession instance
|
||||
:retry_count: times to retry before returning the exceptions
|
||||
:retry_delay: the time to wait before every retry
|
||||
:return: a list containing trades in DEFAULT_TRADES_COLUMNS format
|
||||
"""
|
||||
|
||||
url = binance_vision_trades_zip_url(symbol, candle_type, date)
|
||||
|
||||
logger.debug(f"download trades data from binance: {url}")
|
||||
|
||||
retry = 0
|
||||
while True:
|
||||
if retry > 0:
|
||||
sleep_secs = retry * retry_delay
|
||||
logger.debug(
|
||||
f"[{retry}/{retry_count}] retry to download {url} after {sleep_secs} seconds"
|
||||
)
|
||||
await asyncio.sleep(sleep_secs)
|
||||
try:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
content = await resp.read()
|
||||
logger.debug(f"Successfully downloaded {url}")
|
||||
with zipfile.ZipFile(BytesIO(content)) as zipf:
|
||||
with zipf.open(zipf.namelist()[0]) as csvf:
|
||||
return parse_trades_from_zip(csvf)
|
||||
elif resp.status == 404:
|
||||
logger.debug(f"Failed to download {url}")
|
||||
raise Http404(f"404: {url}", date, url)
|
||||
else:
|
||||
raise BadHttpStatus(f"{resp.status} - {resp.reason}")
|
||||
except Exception as e:
|
||||
logger.info("download Daily_trades raised: %s", e)
|
||||
retry += 1
|
||||
if isinstance(e, Http404) or retry > retry_count:
|
||||
logger.debug(f"Failed to get data from {url}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def _download_archive_trades(
|
||||
symbol: str,
|
||||
pair: str,
|
||||
candle_type: CandleType,
|
||||
start: date,
|
||||
end: date,
|
||||
stop_on_404: bool,
|
||||
) -> list[list]:
|
||||
# daily dataframes, `None` indicates missing data in that day (when `stop_on_404` is False)
|
||||
results: list[list] = []
|
||||
# the current day being processing, starting at 1.
|
||||
current_day = 0
|
||||
|
||||
connector = aiohttp.TCPConnector(limit=100)
|
||||
async with aiohttp.ClientSession(connector=connector, trust_env=True) as session:
|
||||
# the HTTP connections has been throttled by TCPConnector
|
||||
for dates in chunks(list(date_range(start, end)), 30):
|
||||
tasks = [
|
||||
asyncio.create_task(get_daily_trades(symbol, candle_type, date, session))
|
||||
for date in dates
|
||||
]
|
||||
for task in tasks:
|
||||
current_day += 1
|
||||
try:
|
||||
result = await task
|
||||
except Http404 as e:
|
||||
if stop_on_404:
|
||||
logger.debug(f"Failed to download {e.url} due to 404.")
|
||||
|
||||
# A 404 error on the first day indicates missing data
|
||||
# on https://data.binance.vision, we provide the warning and the advice.
|
||||
# https://github.com/freqtrade/freqtrade/blob/acc53065e5fa7ab5197073276306dc9dc3adbfa3/tests/exchange_online/test_binance_compare_ohlcv.py#L7
|
||||
if current_day == 1:
|
||||
logger.warning(
|
||||
f"Fast download is unavailable due to missing data: "
|
||||
f"{e.url}. Falling back to the slower REST API, "
|
||||
"which may take more time."
|
||||
)
|
||||
if pair in ["BTC/USDT:USDT", "ETH/USDT:USDT", "BCH/USDT:USDT"]:
|
||||
logger.warning(
|
||||
f"To avoid the delay, you can first download {pair} using "
|
||||
"`--timerange <start date>-20200101`, and then download the "
|
||||
"remaining data with `--timerange 20200101-<end date>`."
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Binance fast download for {pair} stopped at {e.date} due to "
|
||||
f"missing data: {e.url}, falling back to rest API for the "
|
||||
"remaining data, this can take more time."
|
||||
)
|
||||
await cancel_and_await_tasks(tasks[tasks.index(task) + 1 :])
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.warning(f"An exception raised: {e}")
|
||||
# Directly return the existing data, do not allow the gap within the data
|
||||
await cancel_and_await_tasks(tasks[tasks.index(task) + 1 :])
|
||||
return results
|
||||
else:
|
||||
# Happy case
|
||||
results.extend(result)
|
||||
|
||||
return results
|
||||
|
||||
+11
-10
@@ -31,7 +31,6 @@ class Bybit(Exchange):
|
||||
unified_account = False
|
||||
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"ohlcv_has_history": True,
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||
"ws_enabled": True,
|
||||
@@ -167,15 +166,16 @@ class Bybit(Exchange):
|
||||
PERPETUAL:
|
||||
bybit:
|
||||
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
|
||||
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
|
||||
|
||||
Long:
|
||||
Liquidation Price = (
|
||||
Entry Price * (1 - Initial Margin Rate + Maintenance Margin Rate)
|
||||
- Extra Margin Added/ Contract)
|
||||
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
- (Extra Margin Added/Contract Quantity))
|
||||
Short:
|
||||
Liquidation Price = (
|
||||
Entry Price * (1 + Initial Margin Rate - Maintenance Margin Rate)
|
||||
+ Extra Margin Added/ Contract)
|
||||
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
+ (Extra Margin Added/Contract Quantity))
|
||||
|
||||
Implementation Note: Extra margin is currently not used.
|
||||
|
||||
@@ -185,8 +185,6 @@ class Bybit(Exchange):
|
||||
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||
:param stake_amount: Stake amount - Collateral in settle currency.
|
||||
:param leverage: Leverage used for this position.
|
||||
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
||||
:param margin_mode: Either ISOLATED or CROSS
|
||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||
Cross-Margin Mode: crossWalletBalance
|
||||
Isolated-Margin Mode: isolatedWalletBalance
|
||||
@@ -199,13 +197,16 @@ class Bybit(Exchange):
|
||||
if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
|
||||
if market["inverse"]:
|
||||
raise OperationalException("Freqtrade does not yet support inverse contracts")
|
||||
initial_margin_rate = 1 / leverage
|
||||
position_value = amount * open_rate
|
||||
initial_margin = position_value / leverage
|
||||
maintenance_margin = position_value * mm_ratio
|
||||
margin_diff_per_contract = (initial_margin - maintenance_margin) / amount
|
||||
|
||||
# See docstring - ignores extra margin!
|
||||
if is_short:
|
||||
return open_rate * (1 + initial_margin_rate - mm_ratio)
|
||||
return open_rate + margin_diff_per_contract
|
||||
else:
|
||||
return open_rate * (1 - initial_margin_rate + mm_ratio)
|
||||
return open_rate - margin_diff_per_contract
|
||||
|
||||
else:
|
||||
raise OperationalException(
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
"""CoinbasePro exchange subclass"""
|
||||
|
||||
import logging
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Coinbasepro(Exchange):
|
||||
"""
|
||||
CoinbasePro exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
|
||||
Please note that this exchange is not included in the list of exchanges
|
||||
officially supported by the Freqtrade development team. So some features
|
||||
may still not work as expected.
|
||||
"""
|
||||
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 300,
|
||||
}
|
||||
@@ -12,7 +12,7 @@ from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import floor, isnan
|
||||
from threading import Lock
|
||||
from typing import Any, Literal, TypeGuard
|
||||
from typing import Any, Literal, TypeGuard, TypeVar
|
||||
|
||||
import ccxt
|
||||
import ccxt.pro as ccxt_pro
|
||||
@@ -113,6 +113,8 @@ from freqtrade.util.periodic_cache import PeriodicCache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Exchange:
|
||||
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
||||
@@ -131,7 +133,6 @@ class Exchange:
|
||||
"stoploss_order_types": {},
|
||||
"order_time_in_force": ["GTC"],
|
||||
"ohlcv_params": {},
|
||||
"ohlcv_candle_limit": 500,
|
||||
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
||||
"ohlcv_partial_candle": True,
|
||||
"ohlcv_require_since": False,
|
||||
@@ -276,6 +277,11 @@ class Exchange:
|
||||
|
||||
logger.info(f'Using Exchange "{self.name}"')
|
||||
self.required_candle_call_count = 1
|
||||
# Converts the interval provided in minutes in config to seconds
|
||||
self.markets_refresh_interval: int = (
|
||||
exchange_conf.get("markets_refresh_interval", 60) * 60 * 1000
|
||||
)
|
||||
|
||||
if validate:
|
||||
# Initial markets load
|
||||
self.reload_markets(True, load_leverage_tiers=False)
|
||||
@@ -285,11 +291,6 @@ class Exchange:
|
||||
self._startup_candle_count, config.get("timeframe", "")
|
||||
)
|
||||
|
||||
# Converts the interval provided in minutes in config to seconds
|
||||
self.markets_refresh_interval: int = (
|
||||
exchange_conf.get("markets_refresh_interval", 60) * 60 * 1000
|
||||
)
|
||||
|
||||
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
||||
self.fill_leverage_tiers()
|
||||
self.additional_exchange_init()
|
||||
@@ -466,7 +467,12 @@ class Exchange:
|
||||
:return: Candle limit as integer
|
||||
"""
|
||||
|
||||
fallback_val = self._ft_has.get("ohlcv_candle_limit")
|
||||
ccxt_val = self.features(
|
||||
"spot" if candle_type == CandleType.SPOT else "futures", "fetchOHLCV", "limit", 500
|
||||
)
|
||||
if not isinstance(ccxt_val, float | int):
|
||||
ccxt_val = 500
|
||||
fallback_val = self._ft_has.get("ohlcv_candle_limit", ccxt_val)
|
||||
if candle_type == CandleType.FUNDING_RATE:
|
||||
fallback_val = self._ft_has.get("funding_fee_candle_limit", fallback_val)
|
||||
return int(
|
||||
@@ -642,7 +648,8 @@ class Exchange:
|
||||
|
||||
def _load_async_markets(self, reload: bool = False) -> dict[str, Any]:
|
||||
try:
|
||||
markets = self.loop.run_until_complete(self._api_reload_markets(reload=reload))
|
||||
with self._loop_lock:
|
||||
markets = self.loop.run_until_complete(self._api_reload_markets(reload=reload))
|
||||
|
||||
if isinstance(markets, Exception):
|
||||
raise markets
|
||||
@@ -887,6 +894,24 @@ class Exchange:
|
||||
return self._ft_has["exchange_has_overrides"][endpoint]
|
||||
return endpoint in self._api_async.has and self._api_async.has[endpoint]
|
||||
|
||||
def features(
|
||||
self, market_type: Literal["spot", "futures"], endpoint, attribute, default: T
|
||||
) -> T:
|
||||
"""
|
||||
Returns the exchange features for the given markettype
|
||||
https://docs.ccxt.com/#/README?id=features
|
||||
attributes are in a nested dict, with spot and swap.linear
|
||||
e.g. spot.fetchOHLCV.limit
|
||||
swap.linear.fetchOHLCV.limit
|
||||
"""
|
||||
feat = (
|
||||
self._api_async.features.get("spot", {})
|
||||
if market_type == "spot"
|
||||
else self._api_async.features.get("swap", {}).get("linear", {})
|
||||
)
|
||||
|
||||
return feat.get(endpoint, {}).get(attribute, default)
|
||||
|
||||
def get_precision_amount(self, pair: str) -> float | None:
|
||||
"""
|
||||
Returns the amount precision of the exchange.
|
||||
@@ -2318,15 +2343,17 @@ class Exchange:
|
||||
:param until_ms: Timestamp in milliseconds to get history up to
|
||||
:return: Dataframe with candle (OHLCV) data
|
||||
"""
|
||||
pair, _, _, data, _ = self.loop.run_until_complete(
|
||||
self._async_get_historic_ohlcv(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms,
|
||||
until_ms=until_ms,
|
||||
candle_type=candle_type,
|
||||
with self._loop_lock:
|
||||
pair, _, _, data, _ = self.loop.run_until_complete(
|
||||
self._async_get_historic_ohlcv(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms,
|
||||
until_ms=until_ms,
|
||||
candle_type=candle_type,
|
||||
raise_=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -2365,7 +2392,7 @@ class Exchange:
|
||||
if isinstance(res, BaseException):
|
||||
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||
if raise_:
|
||||
raise
|
||||
raise res
|
||||
continue
|
||||
else:
|
||||
# Deconstruct tuple if it's not an exception
|
||||
@@ -2414,8 +2441,8 @@ class Exchange:
|
||||
|
||||
return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_ts)
|
||||
logger.info(
|
||||
f"Failed to reuse watch {pair}, {timeframe}, {candle_ts < last_refresh_time},"
|
||||
f" {candle_ts}, {last_refresh_time}, "
|
||||
f"Couldn't reuse watch for {pair}, {timeframe}, falling back to REST api. "
|
||||
f"{candle_ts < last_refresh_time}, {candle_ts}, {last_refresh_time}, "
|
||||
f"{format_ms_time(candle_ts)}, {format_ms_time(last_refresh_time)} "
|
||||
)
|
||||
|
||||
@@ -3661,12 +3688,12 @@ class Exchange:
|
||||
def dry_run_liquidation_price(
|
||||
self,
|
||||
pair: str,
|
||||
open_rate: float, # Entry price of position
|
||||
open_rate: float,
|
||||
is_short: bool,
|
||||
amount: float,
|
||||
stake_amount: float,
|
||||
leverage: float,
|
||||
wallet_balance: float, # Or margin balance
|
||||
wallet_balance: float,
|
||||
open_trades: list,
|
||||
) -> float | None:
|
||||
"""
|
||||
@@ -3687,8 +3714,6 @@ class Exchange:
|
||||
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||
:param stake_amount: Stake amount - Collateral in settle currency.
|
||||
:param leverage: Leverage used for this position.
|
||||
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
||||
:param margin_mode: Either ISOLATED or CROSS
|
||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||
Cross-Margin Mode: crossWalletBalance
|
||||
Isolated-Margin Mode: isolatedWalletBalance
|
||||
|
||||
@@ -4,7 +4,7 @@ Exchange support utils
|
||||
|
||||
import inspect
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import ceil, floor
|
||||
from math import ceil, floor, isnan
|
||||
from typing import Any
|
||||
|
||||
import ccxt
|
||||
@@ -305,7 +305,7 @@ def price_to_precision(
|
||||
:param rounding_mode: rounding mode to use. Defaults to ROUND
|
||||
:return: price rounded up to the precision the Exchange accepts
|
||||
"""
|
||||
if price_precision is not None and precisionMode is not None:
|
||||
if price_precision is not None and precisionMode is not None and not isnan(price):
|
||||
if rounding_mode not in (ROUND_UP, ROUND_DOWN):
|
||||
# Use CCXT code where possible.
|
||||
return float(
|
||||
|
||||
@@ -9,6 +9,8 @@ import ccxt
|
||||
|
||||
from freqtrade.constants import Config, PairWithTimeframe
|
||||
from freqtrade.enums.candletype import CandleType
|
||||
from freqtrade.exceptions import TemporaryError
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange import timeframe_to_seconds
|
||||
from freqtrade.exchange.exchange_types import OHLCVResponse
|
||||
from freqtrade.util import dt_ts, format_ms_time, format_ms_time_det
|
||||
@@ -82,14 +84,21 @@ class ExchangeWS:
|
||||
Remove history for a pair/timeframe combination from ccxt cache
|
||||
"""
|
||||
self._ccxt_object.ohlcvs.get(paircomb[0], {}).pop(paircomb[1], None)
|
||||
self.klines_last_refresh.pop(paircomb, None)
|
||||
|
||||
@retrier(retries=3)
|
||||
def ohlcvs(self, pair: str, timeframe: str) -> list[list]:
|
||||
"""
|
||||
Returns a copy of the klines for a pair/timeframe combination
|
||||
Note: this will only contain the data received from the websocket
|
||||
so the data will build up over time.
|
||||
"""
|
||||
return deepcopy(self._ccxt_object.ohlcvs.get(pair, {}).get(timeframe, []))
|
||||
try:
|
||||
return deepcopy(self._ccxt_object.ohlcvs.get(pair, {}).get(timeframe, []))
|
||||
except RuntimeError as e:
|
||||
# Capture runtime errors and retry
|
||||
# TemporaryError does not cause backoff - so we're essentially retrying immediately
|
||||
raise TemporaryError(f"Error deepcopying: {e}") from e
|
||||
|
||||
def cleanup_expired(self) -> None:
|
||||
"""
|
||||
@@ -130,6 +139,15 @@ class ExchangeWS:
|
||||
)
|
||||
)
|
||||
|
||||
async def _unwatch_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None:
|
||||
try:
|
||||
await self._ccxt_object.un_watch_ohlcv_for_symbols([[pair, timeframe]])
|
||||
except ccxt.NotSupported as e:
|
||||
logger.debug("un_watch_ohlcv_for_symbols not supported: %s", e)
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception("Exception in _unwatch_ohlcv")
|
||||
|
||||
def _continuous_stopped(
|
||||
self, task: asyncio.Task, pair: str, timeframe: str, candle_type: CandleType
|
||||
):
|
||||
@@ -142,6 +160,10 @@ class ExchangeWS:
|
||||
result = str(result1)
|
||||
|
||||
logger.info(f"{pair}, {timeframe}, {candle_type} - Task finished - {result}")
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._unwatch_ohlcv(pair, timeframe, candle_type), loop=self._loop
|
||||
)
|
||||
|
||||
self._klines_scheduled.discard((pair, timeframe, candle_type))
|
||||
self._pop_history((pair, timeframe, candle_type))
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ class Gate(Exchange):
|
||||
unified_account = False
|
||||
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"order_time_in_force": ["GTC", "IOC"],
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
|
||||
@@ -21,7 +21,6 @@ class Htx(Exchange):
|
||||
"stop_price_param": "stopPrice",
|
||||
"stop_price_prop": "stopPrice",
|
||||
"stoploss_order_types": {"limit": "stop-limit"},
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"l2_limit_range": [5, 10, 20],
|
||||
"l2_limit_range_required": False,
|
||||
"ohlcv_candle_limit_per_timeframe": {
|
||||
|
||||
@@ -22,7 +22,6 @@ class Hyperliquid(Exchange):
|
||||
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_has_history": False,
|
||||
"ohlcv_candle_limit": 5000,
|
||||
"l2_limit_range": [20],
|
||||
"trades_has_history": False,
|
||||
"tickers_have_bid_ask": False,
|
||||
|
||||
@@ -26,7 +26,6 @@ class Kraken(Exchange):
|
||||
"stop_price_prop": "stopLossPrice",
|
||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||
"order_time_in_force": ["GTC", "IOC", "PO"],
|
||||
"ohlcv_candle_limit": 720,
|
||||
"ohlcv_has_history": False,
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "since",
|
||||
|
||||
@@ -28,7 +28,6 @@ class Kucoin(Exchange):
|
||||
"l2_limit_range": [20, 100],
|
||||
"l2_limit_range_required": False,
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC"],
|
||||
"ohlcv_candle_limit": 1500,
|
||||
}
|
||||
|
||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> dict:
|
||||
|
||||
@@ -46,19 +46,20 @@ class BaseEnvironment(gym.Env):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
df: DataFrame = DataFrame(),
|
||||
prices: DataFrame = DataFrame(),
|
||||
reward_kwargs: dict = {},
|
||||
*,
|
||||
df: DataFrame,
|
||||
prices: DataFrame,
|
||||
reward_kwargs: dict,
|
||||
window_size=10,
|
||||
starting_point=True,
|
||||
id: str = "baseenv-1", # noqa: A002
|
||||
seed: int = 1,
|
||||
config: dict = {},
|
||||
config: dict,
|
||||
live: bool = False,
|
||||
fee: float = 0.0015,
|
||||
can_short: bool = False,
|
||||
pair: str = "",
|
||||
df_raw: DataFrame = DataFrame(),
|
||||
df_raw: DataFrame,
|
||||
):
|
||||
"""
|
||||
Initializes the training/eval environment.
|
||||
|
||||
@@ -488,7 +488,7 @@ def make_env(
|
||||
seed: int,
|
||||
train_df: DataFrame,
|
||||
price: DataFrame,
|
||||
env_info: dict[str, Any] = {},
|
||||
env_info: dict[str, Any],
|
||||
) -> Callable:
|
||||
"""
|
||||
Utility function for multiprocessed env.
|
||||
|
||||
@@ -6,26 +6,21 @@ from sklearn.utils.validation import has_fit_parameter, validate_data
|
||||
class FreqaiMultiOutputRegressor(MultiOutputRegressor):
|
||||
def fit(self, X, y, sample_weight=None, fit_params=None):
|
||||
"""Fit the model to data, separately for each output variable.
|
||||
Parameters
|
||||
----------
|
||||
X : {array-like, sparse matrix} of shape (n_samples, n_features)
|
||||
:param X: {array-like, sparse matrix} of shape (n_samples, n_features)
|
||||
The input data.
|
||||
y : {array-like, sparse matrix} of shape (n_samples, n_outputs)
|
||||
:param y: {array-like, sparse matrix} of shape (n_samples, n_outputs)
|
||||
Multi-output targets. An indicator matrix turns on multilabel
|
||||
estimation.
|
||||
sample_weight : array-like of shape (n_samples,), default=None
|
||||
:param sample_weight: array-like of shape (n_samples,), default=None
|
||||
Sample weights. If `None`, then samples are equally weighted.
|
||||
Only supported if the underlying regressor supports sample
|
||||
weights.
|
||||
fit_params : A list of dicts for the fit_params
|
||||
|
||||
:param fit_params: A list of dicts for the fit_params
|
||||
Parameters passed to the ``estimator.fit`` method of each step.
|
||||
Each dict may contain same or different values (e.g. different
|
||||
eval_sets or init_models)
|
||||
.. versionadded:: 0.23
|
||||
Returns
|
||||
-------
|
||||
self : object
|
||||
Returns a fitted instance.
|
||||
|
||||
"""
|
||||
|
||||
if not hasattr(self.estimator, "fit"):
|
||||
|
||||
@@ -33,6 +33,8 @@ LABEL_PIPELINE = "label_pipeline"
|
||||
TRAINDF = "trained_df"
|
||||
METADATA = "metadata"
|
||||
|
||||
METADATA_NUMBER_MODE = rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||
|
||||
|
||||
class pair_info(TypedDict):
|
||||
model_filename: str
|
||||
@@ -495,7 +497,7 @@ class FreqaiDataDrawer:
|
||||
dk.data["label_list"] = dk.label_list
|
||||
|
||||
with (save_path / f"{dk.model_filename}_{METADATA}.json").open("w") as fp:
|
||||
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE)
|
||||
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=METADATA_NUMBER_MODE)
|
||||
|
||||
return
|
||||
|
||||
@@ -526,7 +528,7 @@ class FreqaiDataDrawer:
|
||||
dk.data["label_list"] = dk.label_list
|
||||
# store the metadata
|
||||
with (save_path / f"{dk.model_filename}_{METADATA}.json").open("w") as fp:
|
||||
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE)
|
||||
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=METADATA_NUMBER_MODE)
|
||||
|
||||
# save the pipelines to pickle files
|
||||
with (save_path / f"{dk.model_filename}_{FEATURE_PIPELINE}.pkl").open("wb") as fp:
|
||||
@@ -563,7 +565,7 @@ class FreqaiDataDrawer:
|
||||
presaved backtesting (prediction file loading).
|
||||
"""
|
||||
with (dk.data_path / f"{dk.model_filename}_{METADATA}.json").open("r") as fp:
|
||||
dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||
dk.data = rapidjson.load(fp, number_mode=METADATA_NUMBER_MODE)
|
||||
dk.training_features_list = dk.data["training_features_list"]
|
||||
dk.label_list = dk.data["label_list"]
|
||||
|
||||
@@ -587,7 +589,7 @@ class FreqaiDataDrawer:
|
||||
dk.label_pipeline = self.meta_data_dictionary[coin][LABEL_PIPELINE]
|
||||
else:
|
||||
with (dk.data_path / f"{dk.model_filename}_{METADATA}.json").open("r") as fp:
|
||||
dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||
dk.data = rapidjson.load(fp, number_mode=METADATA_NUMBER_MODE)
|
||||
|
||||
with (dk.data_path / f"{dk.model_filename}_{FEATURE_PIPELINE}.pkl").open("rb") as fp:
|
||||
dk.feature_pipeline = cloudpickle.load(fp)
|
||||
|
||||
@@ -214,7 +214,7 @@ class FreqaiDataKitchen:
|
||||
self,
|
||||
unfiltered_df: DataFrame,
|
||||
training_feature_list: list,
|
||||
label_list: list = list(),
|
||||
label_list: list | None = None,
|
||||
training_filter: bool = True,
|
||||
) -> tuple[DataFrame, DataFrame]:
|
||||
"""
|
||||
@@ -244,7 +244,7 @@ class FreqaiDataKitchen:
|
||||
# we don't care about total row number (total no. datapoints) in training, we only care
|
||||
# about removing any row with NaNs
|
||||
# if labels has multiple columns (user wants to train multiple modelEs), we detect here
|
||||
labels = unfiltered_df.filter(label_list, axis=1)
|
||||
labels = unfiltered_df.filter(label_list or [], axis=1)
|
||||
drop_index_labels = pd.isnull(labels).any(axis=1)
|
||||
drop_index_labels = (
|
||||
drop_index_labels.replace(True, 1).replace(False, 0).infer_objects(copy=False)
|
||||
@@ -654,8 +654,8 @@ class FreqaiDataKitchen:
|
||||
pair: str,
|
||||
tf: str,
|
||||
strategy: IStrategy,
|
||||
corr_dataframes: dict = {},
|
||||
base_dataframes: dict = {},
|
||||
corr_dataframes: dict,
|
||||
base_dataframes: dict,
|
||||
is_corr_pairs: bool = False,
|
||||
) -> DataFrame:
|
||||
"""
|
||||
@@ -773,10 +773,10 @@ class FreqaiDataKitchen:
|
||||
def use_strategy_to_populate_indicators( # noqa: C901
|
||||
self,
|
||||
strategy: IStrategy,
|
||||
corr_dataframes: dict = {},
|
||||
base_dataframes: dict = {},
|
||||
corr_dataframes: dict[str, DataFrame] | None = None,
|
||||
base_dataframes: dict[str, dict[str, DataFrame]] | None = None,
|
||||
pair: str = "",
|
||||
prediction_dataframe: DataFrame = pd.DataFrame(),
|
||||
prediction_dataframe: DataFrame | None = None,
|
||||
do_corr_pairs: bool = True,
|
||||
) -> DataFrame:
|
||||
"""
|
||||
@@ -793,6 +793,10 @@ class FreqaiDataKitchen:
|
||||
:return:
|
||||
dataframe: DataFrame = dataframe containing populated indicators
|
||||
"""
|
||||
if not corr_dataframes:
|
||||
corr_dataframes = {}
|
||||
if not base_dataframes:
|
||||
base_dataframes = {}
|
||||
|
||||
# check if the user is using the deprecated populate_any_indicators function
|
||||
new_version = inspect.getsource(strategy.populate_any_indicators) == (
|
||||
@@ -822,7 +826,7 @@ class FreqaiDataKitchen:
|
||||
if tf not in corr_dataframes[p]:
|
||||
corr_dataframes[p][tf] = pd.DataFrame()
|
||||
|
||||
if not prediction_dataframe.empty:
|
||||
if prediction_dataframe is not None and not prediction_dataframe.empty:
|
||||
dataframe = prediction_dataframe.copy()
|
||||
base_dataframes[self.config["timeframe"]] = dataframe.copy()
|
||||
else:
|
||||
|
||||
@@ -618,7 +618,7 @@ class IFreqaiModel(ABC):
|
||||
)
|
||||
|
||||
unfiltered_dataframe = dk.use_strategy_to_populate_indicators(
|
||||
strategy, corr_dataframes, base_dataframes, pair
|
||||
strategy, corr_dataframes=corr_dataframes, base_dataframes=base_dataframes, pair=pair
|
||||
)
|
||||
|
||||
trained_timestamp = new_trained_timerange.stopts
|
||||
|
||||
@@ -25,7 +25,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
||||
criterion: nn.Module,
|
||||
device: str,
|
||||
data_convertor: PyTorchDataConvertor,
|
||||
model_meta_data: dict[str, Any] = {},
|
||||
model_meta_data: dict[str, Any] | None = None,
|
||||
window_size: int = 1,
|
||||
tb_logger: Any = None,
|
||||
**kwargs,
|
||||
@@ -45,6 +45,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
||||
:param n_epochs: The maximum number batches to use for evaluation.
|
||||
:param batch_size: The size of the batches to use during training.
|
||||
"""
|
||||
if model_meta_data is None:
|
||||
model_meta_data = {}
|
||||
self.model = model
|
||||
self.optimizer = optimizer
|
||||
self.criterion = criterion
|
||||
|
||||
+127
-59
@@ -64,7 +64,7 @@ from freqtrade.rpc.rpc_types import (
|
||||
)
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.util import FtPrecise, MeasureTime, dt_from_ts
|
||||
from freqtrade.util import FtPrecise, MeasureTime, PeriodicCache, dt_from_ts, dt_now
|
||||
from freqtrade.util.migrations.binance_mig import migrate_binance_futures_names
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
@@ -145,7 +145,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
else None
|
||||
)
|
||||
|
||||
self.active_pair_whitelist = self._refresh_active_whitelist()
|
||||
logger.info("Starting initial pairlist refresh")
|
||||
with MeasureTime(
|
||||
lambda duration, _: logger.info(f"Initial Pairlist refresh took {duration:.2f}s"), 0
|
||||
):
|
||||
self.active_pair_whitelist = self._refresh_active_whitelist()
|
||||
|
||||
# Set initial bot state from config
|
||||
initial_state = self.config.get("initial_state")
|
||||
@@ -154,6 +158,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Protect exit-logic from forcesell and vice versa
|
||||
self._exit_lock = Lock()
|
||||
timeframe_secs = timeframe_to_seconds(self.strategy.timeframe)
|
||||
self._exit_reason_cache = PeriodicCache(100, ttl=timeframe_secs)
|
||||
LoggingMixin.__init__(self, logger, timeframe_secs)
|
||||
|
||||
self._schedule = Scheduler()
|
||||
@@ -784,6 +789,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
return
|
||||
else:
|
||||
logger.debug("Max adjustment entries is set to unlimited.")
|
||||
|
||||
self.execute_entry(
|
||||
trade.pair,
|
||||
stake_amount,
|
||||
@@ -898,14 +904,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
msg = (
|
||||
f"Position adjust: about to create a new order for {pair} with stake_amount: "
|
||||
f"{stake_amount} for {trade}"
|
||||
f"{stake_amount} and price: {enter_limit_requested} for {trade}"
|
||||
if mode == "pos_adjust"
|
||||
else (
|
||||
f"Replacing {side} order: about create a new order for {pair} with stake_amount: "
|
||||
f"{stake_amount} ..."
|
||||
f"{stake_amount} and price: {enter_limit_requested} ..."
|
||||
if mode == "replace"
|
||||
else f"{name} signal found: about create a new trade for {pair} with stake_amount: "
|
||||
f"{stake_amount} ..."
|
||||
f"{stake_amount} and price: {enter_limit_requested} ..."
|
||||
)
|
||||
)
|
||||
logger.info(msg)
|
||||
@@ -1374,6 +1380,15 @@ class FreqtradeBot(LoggingMixin):
|
||||
for should_exit in exits:
|
||||
if should_exit.exit_flag:
|
||||
exit_tag1 = exit_tag if should_exit.exit_type == ExitType.EXIT_SIGNAL else None
|
||||
if trade.has_open_orders:
|
||||
if prev_eval := self._exit_reason_cache.get(
|
||||
f"{trade.pair}_{trade.id}_{exit_tag1 or should_exit.exit_reason}", None
|
||||
):
|
||||
logger.debug(
|
||||
f"Exit reason already seen this candle, first seen at {prev_eval}"
|
||||
)
|
||||
continue
|
||||
|
||||
logger.info(
|
||||
f"Exit for {trade.pair} detected. Reason: {should_exit.exit_type}"
|
||||
f"{f' Tag: {exit_tag1}' if exit_tag1 is not None else ''}"
|
||||
@@ -1593,27 +1608,29 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.replace_order(order, open_order, trade)
|
||||
|
||||
def handle_cancel_order(
|
||||
self, order: CcxtOrder, order_obj: Order, trade: Trade, reason: str
|
||||
) -> None:
|
||||
self, order: CcxtOrder, order_obj: Order, trade: Trade, reason: str, replacing: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Check if current analyzed order timed out and cancel if necessary.
|
||||
:param order: Order dict grabbed with exchange.fetch_order()
|
||||
:param order_obj: Order object from the database.
|
||||
:param trade: Trade object.
|
||||
:return: None
|
||||
:return: True if the order was canceled, False otherwise.
|
||||
"""
|
||||
if order["side"] == trade.entry_side:
|
||||
self.handle_cancel_enter(trade, order, order_obj, reason)
|
||||
return self.handle_cancel_enter(trade, order, order_obj, reason, replacing)
|
||||
else:
|
||||
canceled = self.handle_cancel_exit(trade, order, order_obj, reason)
|
||||
canceled_count = trade.get_canceled_exit_order_count()
|
||||
max_timeouts = self.config.get("unfilledtimeout", {}).get("exit_timeout_count", 0)
|
||||
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||
logger.warning(
|
||||
f"Emergency exiting trade {trade}, as the exit order "
|
||||
f"timed out {max_timeouts} times. force selling {order['amount']}."
|
||||
)
|
||||
self.emergency_exit(trade, order["price"], order["amount"])
|
||||
if not replacing:
|
||||
canceled_count = trade.get_canceled_exit_order_count()
|
||||
max_timeouts = self.config.get("unfilledtimeout", {}).get("exit_timeout_count", 0)
|
||||
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||
logger.warning(
|
||||
f"Emergency exiting trade {trade}, as the exit order "
|
||||
f"timed out {max_timeouts} times. force selling {order['amount']}."
|
||||
)
|
||||
self.emergency_exit(trade, order["price"], order["amount"])
|
||||
return canceled
|
||||
|
||||
def emergency_exit(
|
||||
self, trade: Trade, price: float, sub_trade_amt: float | None = None
|
||||
@@ -1649,9 +1666,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
def replace_order(self, order: CcxtOrder, order_obj: Order | None, trade: Trade) -> None:
|
||||
"""
|
||||
Check if current analyzed entry order should be replaced or simply cancelled.
|
||||
To simply cancel the existing order(no replacement) adjust_entry_price() should return None
|
||||
To maintain existing order adjust_entry_price() should return order_obj.price
|
||||
To replace existing order adjust_entry_price() should return desired price for limit order
|
||||
To simply cancel the existing order(no replacement) adjust_order_price() should return None
|
||||
To maintain existing order adjust_order_price() should return order_obj.price
|
||||
To replace existing order adjust_order_price() should return desired price for limit order
|
||||
:param order: Order dict grabbed with exchange.fetch_order()
|
||||
:param order_obj: Order object.
|
||||
:param trade: Trade object.
|
||||
@@ -1665,17 +1682,17 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.strategy.timeframe, latest_candle_open_date
|
||||
)
|
||||
# Check if new candle
|
||||
if (
|
||||
order_obj
|
||||
and order_obj.side == trade.entry_side
|
||||
and latest_candle_close_date > order_obj.order_date_utc
|
||||
):
|
||||
if order_obj and latest_candle_close_date > order_obj.order_date_utc:
|
||||
is_entry = order_obj.side == trade.entry_side
|
||||
# New candle
|
||||
proposed_rate = self.exchange.get_rate(
|
||||
trade.pair, side="entry", is_short=trade.is_short, refresh=True
|
||||
trade.pair,
|
||||
side="entry" if is_entry else "exit",
|
||||
is_short=trade.is_short,
|
||||
refresh=True,
|
||||
)
|
||||
adjusted_entry_price = strategy_safe_wrapper(
|
||||
self.strategy.adjust_entry_price, default_retval=order_obj.safe_placement_price
|
||||
adjusted_price = strategy_safe_wrapper(
|
||||
self.strategy.adjust_order_price, default_retval=order_obj.safe_placement_price
|
||||
)(
|
||||
trade=trade,
|
||||
order=order_obj,
|
||||
@@ -1685,42 +1702,78 @@ class FreqtradeBot(LoggingMixin):
|
||||
current_order_rate=order_obj.safe_placement_price,
|
||||
entry_tag=trade.enter_tag,
|
||||
side=trade.trade_direction,
|
||||
is_entry=is_entry,
|
||||
)
|
||||
|
||||
replacing = True
|
||||
cancel_reason = constants.CANCEL_REASON["REPLACE"]
|
||||
if not adjusted_entry_price:
|
||||
if not adjusted_price:
|
||||
replacing = False
|
||||
cancel_reason = constants.CANCEL_REASON["USER_CANCEL"]
|
||||
if order_obj.safe_placement_price != adjusted_entry_price:
|
||||
# cancel existing order if new price is supplied or None
|
||||
res = self.handle_cancel_enter(
|
||||
trade, order, order_obj, cancel_reason, replacing=replacing
|
||||
|
||||
if order_obj.safe_placement_price != adjusted_price:
|
||||
self.handle_replace_order(
|
||||
order,
|
||||
order_obj,
|
||||
trade,
|
||||
adjusted_price,
|
||||
is_entry,
|
||||
cancel_reason,
|
||||
replacing=replacing,
|
||||
)
|
||||
if not res:
|
||||
self.replace_order_failed(
|
||||
trade, f"Could not fully cancel order for {trade}, therefore not replacing."
|
||||
|
||||
def handle_replace_order(
|
||||
self,
|
||||
order: CcxtOrder | None,
|
||||
order_obj: Order,
|
||||
trade: Trade,
|
||||
new_order_price: float | None,
|
||||
is_entry: bool,
|
||||
cancel_reason: str,
|
||||
replacing: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Cancel existing order if new price is supplied, and if the cancel is successful,
|
||||
places a new order with the remaining capital.
|
||||
"""
|
||||
if not order:
|
||||
order = self.exchange.fetch_order(order_obj.order_id, trade.pair)
|
||||
res = self.handle_cancel_order(order, order_obj, trade, cancel_reason, replacing=replacing)
|
||||
if not res:
|
||||
self.replace_order_failed(
|
||||
trade, f"Could not fully cancel order for {trade}, therefore not replacing."
|
||||
)
|
||||
return
|
||||
if new_order_price:
|
||||
# place new order only if new price is supplied
|
||||
try:
|
||||
if is_entry:
|
||||
succeeded = self.execute_entry(
|
||||
pair=trade.pair,
|
||||
stake_amount=(
|
||||
order_obj.safe_remaining * order_obj.safe_price / trade.leverage
|
||||
),
|
||||
price=new_order_price,
|
||||
trade=trade,
|
||||
is_short=trade.is_short,
|
||||
mode="replace",
|
||||
)
|
||||
return
|
||||
if adjusted_entry_price:
|
||||
# place new order only if new price is supplied
|
||||
try:
|
||||
if not self.execute_entry(
|
||||
pair=trade.pair,
|
||||
stake_amount=(
|
||||
order_obj.safe_remaining * order_obj.safe_price / trade.leverage
|
||||
),
|
||||
price=adjusted_entry_price,
|
||||
trade=trade,
|
||||
is_short=trade.is_short,
|
||||
mode="replace",
|
||||
):
|
||||
self.replace_order_failed(
|
||||
trade, f"Could not replace order for {trade}."
|
||||
)
|
||||
except DependencyException as exception:
|
||||
logger.warning(f"Unable to replace order for {trade.pair}: {exception}")
|
||||
self.replace_order_failed(trade, f"Could not replace order for {trade}.")
|
||||
else:
|
||||
succeeded = self.execute_trade_exit(
|
||||
trade,
|
||||
new_order_price,
|
||||
exit_check=ExitCheckTuple(
|
||||
exit_type=ExitType.CUSTOM_EXIT,
|
||||
exit_reason=order_obj.ft_order_tag or "order_replaced",
|
||||
),
|
||||
ordertype="limit",
|
||||
sub_trade_amt=order_obj.safe_remaining,
|
||||
)
|
||||
if not succeeded:
|
||||
self.replace_order_failed(trade, f"Could not replace order for {trade}.")
|
||||
except DependencyException as exception:
|
||||
logger.warning(f"Unable to replace order for {trade.pair}: {exception}")
|
||||
self.replace_order_failed(trade, f"Could not replace order for {trade}.")
|
||||
|
||||
def cancel_open_orders_of_trade(
|
||||
self, trade: Trade, sides: list[str], reason: str, replacing: bool = False
|
||||
@@ -1774,7 +1827,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if trade.has_open_orders:
|
||||
oo = trade.select_order(side, True)
|
||||
if oo is not None:
|
||||
if (price == oo.price) and (side == oo.side) and (amount == oo.amount):
|
||||
if price == oo.price and side == oo.side and amount == oo.amount:
|
||||
logger.info(
|
||||
f"A similar open order was found for {trade.pair}. "
|
||||
f"Keeping existing {trade.exit_side} order. {price=}, {amount=}"
|
||||
@@ -1870,7 +1923,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
# to the trade object
|
||||
self.update_trade_state(trade, order_id, corder)
|
||||
|
||||
logger.info(f"Partial {trade.entry_side} order timeout for {trade}.")
|
||||
logger.info(
|
||||
f"Partial {trade.entry_side} order timeout for {trade}. Filled: {filled_amount}, "
|
||||
f"total: {order_obj.ft_amount}"
|
||||
)
|
||||
order_obj.ft_cancel_reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||
|
||||
self.wallets.update()
|
||||
@@ -2092,6 +2148,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.handle_insufficient_funds(trade)
|
||||
return False
|
||||
|
||||
self._exit_reason_cache[f"{trade.pair}_{trade.id}_{exit_reason}"] = dt_now()
|
||||
order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit)
|
||||
order_obj.ft_order_tag = exit_reason
|
||||
trade.orders.append(order_obj)
|
||||
@@ -2555,4 +2612,15 @@ class FreqtradeBot(LoggingMixin):
|
||||
max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r)
|
||||
|
||||
# Bracket between min_custom_price_allowed and max_custom_price_allowed
|
||||
return max(min(valid_custom_price, max_custom_price_allowed), min_custom_price_allowed)
|
||||
final_price = max(
|
||||
min(valid_custom_price, max_custom_price_allowed), min_custom_price_allowed
|
||||
)
|
||||
|
||||
# Log a warning if the custom price was adjusted by clamping.
|
||||
if final_price != valid_custom_price:
|
||||
logger.info(
|
||||
f"Custom price adjusted from {valid_custom_price} to {final_price} based on "
|
||||
"custom_price_max_distance_ratio of {cust_p_max_dist_r}."
|
||||
)
|
||||
|
||||
return final_price
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Any
|
||||
from copy import deepcopy
|
||||
from typing import Any, cast
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
@@ -15,11 +16,16 @@ class BacktestResultType(TypedDict):
|
||||
|
||||
|
||||
def get_BacktestResultType_default() -> BacktestResultType:
|
||||
return {
|
||||
"metadata": {},
|
||||
"strategy": {},
|
||||
"strategy_comparison": [],
|
||||
}
|
||||
return cast(
|
||||
BacktestResultType,
|
||||
deepcopy(
|
||||
{
|
||||
"metadata": {},
|
||||
"strategy": {},
|
||||
"strategy_comparison": [],
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class BacktestHistoryEntryType(BacktestMetadataType):
|
||||
|
||||
@@ -35,19 +35,20 @@ def update_liquidation_prices(
|
||||
|
||||
open_trades: list[Trade] = Trade.get_open_trades()
|
||||
for t in open_trades:
|
||||
# TODO: This should be done in a batch update
|
||||
t.set_liquidation_price(
|
||||
exchange.get_liquidation_price(
|
||||
pair=t.pair,
|
||||
open_rate=t.open_rate,
|
||||
is_short=t.is_short,
|
||||
amount=t.amount,
|
||||
stake_amount=t.stake_amount,
|
||||
leverage=t.leverage,
|
||||
wallet_balance=total_wallet_stake,
|
||||
open_trades=open_trades,
|
||||
if t.has_open_position:
|
||||
# TODO: This should be done in a batch update
|
||||
t.set_liquidation_price(
|
||||
exchange.get_liquidation_price(
|
||||
pair=t.pair,
|
||||
open_rate=t.open_rate,
|
||||
is_short=t.is_short,
|
||||
amount=t.amount,
|
||||
stake_amount=t.stake_amount,
|
||||
leverage=t.leverage,
|
||||
wallet_balance=total_wallet_stake,
|
||||
open_trades=open_trades,
|
||||
)
|
||||
)
|
||||
)
|
||||
elif trade:
|
||||
trade.set_liquidation_price(
|
||||
exchange.get_liquidation_price(
|
||||
|
||||
+154
-54
@@ -1,15 +1,16 @@
|
||||
import logging
|
||||
import logging.config
|
||||
import os
|
||||
from copy import deepcopy
|
||||
from logging import Formatter
|
||||
from logging.handlers import RotatingFileHandler, SysLogHandler
|
||||
from pathlib import Path
|
||||
|
||||
from rich.console import Console
|
||||
from typing import Any
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.loggers.buffering_handler import FTBufferingHandler
|
||||
from freqtrade.loggers.ft_rich_handler import FtRichHandler
|
||||
from freqtrade.loggers.set_log_levels import set_loggers
|
||||
from freqtrade.loggers.rich_console import get_rich_console
|
||||
|
||||
|
||||
# from freqtrade.loggers.std_err_stream_handler import FTStdErrStreamHandler
|
||||
@@ -22,7 +23,8 @@ LOGFORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
bufferHandler = FTBufferingHandler(1000)
|
||||
bufferHandler.setFormatter(Formatter(LOGFORMAT))
|
||||
|
||||
error_console = Console(stderr=True, color_system=None)
|
||||
|
||||
error_console = get_rich_console(stderr=True, color_system=None)
|
||||
|
||||
|
||||
def get_existing_handlers(handlertype):
|
||||
@@ -53,66 +55,140 @@ def setup_logging_pre() -> None:
|
||||
)
|
||||
|
||||
|
||||
def setup_logging(config: Config) -> None:
|
||||
"""
|
||||
Process -v/--verbose, --logfile options
|
||||
"""
|
||||
# Log level
|
||||
verbosity = config["verbosity"]
|
||||
logging.root.addHandler(bufferHandler)
|
||||
if config.get("print_colorized", True):
|
||||
logger.info("Enabling colorized output.")
|
||||
error_console._color_system = error_console._detect_color_system()
|
||||
FT_LOGGING_CONFIG = {
|
||||
"version": 1,
|
||||
# "incremental": True,
|
||||
# "disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"basic": {"format": "%(message)s"},
|
||||
"standard": {
|
||||
"format": LOGFORMAT,
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "freqtrade.loggers.ft_rich_handler.FtRichHandler",
|
||||
"formatter": "basic",
|
||||
},
|
||||
},
|
||||
"root": {
|
||||
"handlers": [
|
||||
"console",
|
||||
# "file",
|
||||
],
|
||||
"level": "INFO",
|
||||
},
|
||||
}
|
||||
|
||||
logfile = config.get("logfile")
|
||||
|
||||
if logfile:
|
||||
def _set_log_levels(
|
||||
log_config: dict[str, Any], verbosity: int = 0, api_verbosity: str = "info"
|
||||
) -> None:
|
||||
"""
|
||||
Set the logging level for the different loggers
|
||||
"""
|
||||
if "loggers" not in log_config:
|
||||
log_config["loggers"] = {}
|
||||
|
||||
# Set default levels for third party libraries
|
||||
third_party_loggers = {
|
||||
"freqtrade": logging.INFO if verbosity <= 1 else logging.DEBUG,
|
||||
"requests": logging.INFO if verbosity <= 1 else logging.DEBUG,
|
||||
"urllib3": logging.INFO if verbosity <= 1 else logging.DEBUG,
|
||||
"httpcore": logging.INFO if verbosity <= 1 else logging.DEBUG,
|
||||
"ccxt.base.exchange": logging.INFO if verbosity <= 2 else logging.DEBUG,
|
||||
"telegram": logging.INFO,
|
||||
"httpx": logging.WARNING,
|
||||
"werkzeug": logging.ERROR if api_verbosity == "error" else logging.INFO,
|
||||
}
|
||||
|
||||
# Add third party loggers to the configuration
|
||||
for logger_name, level in third_party_loggers.items():
|
||||
if logger_name not in log_config["loggers"]:
|
||||
log_config["loggers"][logger_name] = {
|
||||
"level": logging.getLevelName(level),
|
||||
"propagate": True,
|
||||
}
|
||||
|
||||
|
||||
def _add_root_handler(log_config: dict[str, Any], handler_name: str):
|
||||
if handler_name not in log_config["root"]["handlers"]:
|
||||
log_config["root"]["handlers"].append(handler_name)
|
||||
|
||||
|
||||
def _add_formatter(log_config: dict[str, Any], format_name: str, format_: str):
|
||||
if format_name not in log_config["formatters"]:
|
||||
log_config["formatters"][format_name] = {"format": format_}
|
||||
|
||||
|
||||
def _create_log_config(config: Config) -> dict[str, Any]:
|
||||
# Get log_config from user config or use default
|
||||
log_config = config.get("log_config", deepcopy(FT_LOGGING_CONFIG))
|
||||
|
||||
if logfile := config.get("logfile"):
|
||||
s = logfile.split(":")
|
||||
if s[0] == "syslog":
|
||||
# Address can be either a string (socket filename) for Unix domain socket or
|
||||
# a tuple (hostname, port) for UDP socket.
|
||||
# Address can be omitted (i.e. simple 'syslog' used as the value of
|
||||
# config['logfilename']), which defaults to '/dev/log', applicable for most
|
||||
# of the systems.
|
||||
address = (s[1], int(s[2])) if len(s) > 2 else s[1] if len(s) > 1 else "/dev/log"
|
||||
handler_sl = get_existing_handlers(SysLogHandler)
|
||||
if handler_sl:
|
||||
logging.root.removeHandler(handler_sl)
|
||||
handler_sl = SysLogHandler(address=address)
|
||||
# No datetime field for logging into syslog, to allow syslog
|
||||
# to perform reduction of repeating messages if this is set in the
|
||||
# syslog config. The messages should be equal for this.
|
||||
handler_sl.setFormatter(Formatter("%(name)s - %(levelname)s - %(message)s"))
|
||||
logging.root.addHandler(handler_sl)
|
||||
logger.warning(
|
||||
"DEPRECATED: Configuring syslog logging via command line is deprecated."
|
||||
"Please use the log_config option in the configuration file instead."
|
||||
)
|
||||
# Add syslog handler to the config
|
||||
log_config["handlers"]["syslog"] = {
|
||||
"class": "logging.handlers.SysLogHandler",
|
||||
"formatter": "syslog_format",
|
||||
"address": (s[1], int(s[2])) if len(s) > 2 else s[1] if len(s) > 1 else "/dev/log",
|
||||
}
|
||||
|
||||
_add_formatter(log_config, "syslog_format", "%(name)s - %(levelname)s - %(message)s")
|
||||
_add_root_handler(log_config, "syslog")
|
||||
|
||||
elif s[0] == "journald": # pragma: no cover
|
||||
# Check if we have the module available
|
||||
logger.warning(
|
||||
"DEPRECATED: Configuring Journald logging via command line is deprecated."
|
||||
"Please use the log_config option in the configuration file instead."
|
||||
)
|
||||
try:
|
||||
from cysystemd.journal import JournaldLogHandler
|
||||
from cysystemd.journal import JournaldLogHandler # noqa: F401
|
||||
except ImportError:
|
||||
raise OperationalException(
|
||||
"You need the cysystemd python package be installed in "
|
||||
"order to use logging to journald."
|
||||
)
|
||||
handler_jd = get_existing_handlers(JournaldLogHandler)
|
||||
if handler_jd:
|
||||
logging.root.removeHandler(handler_jd)
|
||||
handler_jd = JournaldLogHandler()
|
||||
# No datetime field for logging into journald, to allow syslog
|
||||
# to perform reduction of repeating messages if this is set in the
|
||||
# syslog config. The messages should be equal for this.
|
||||
handler_jd.setFormatter(Formatter("%(name)s - %(levelname)s - %(message)s"))
|
||||
logging.root.addHandler(handler_jd)
|
||||
|
||||
# Add journald handler to the config
|
||||
log_config["handlers"]["journald"] = {
|
||||
"class": "cysystemd.journal.JournaldLogHandler",
|
||||
"formatter": "journald_format",
|
||||
}
|
||||
|
||||
_add_formatter(log_config, "journald_format", "%(name)s - %(levelname)s - %(message)s")
|
||||
_add_root_handler(log_config, "journald")
|
||||
|
||||
else:
|
||||
handler_rf = get_existing_handlers(RotatingFileHandler)
|
||||
if handler_rf:
|
||||
logging.root.removeHandler(handler_rf)
|
||||
# Regular file logging
|
||||
# Update existing file handler configuration
|
||||
if "file" in log_config["handlers"]:
|
||||
log_config["handlers"]["file"]["filename"] = logfile
|
||||
else:
|
||||
log_config["handlers"]["file"] = {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"formatter": "standard",
|
||||
"filename": logfile,
|
||||
"maxBytes": 1024 * 1024 * 10, # 10Mb
|
||||
"backupCount": 10,
|
||||
}
|
||||
_add_root_handler(log_config, "file")
|
||||
|
||||
# Dynamically update some handlers
|
||||
for handler_config in log_config.get("handlers", {}).values():
|
||||
if handler_config.get("class") == "freqtrade.loggers.ft_rich_handler.FtRichHandler":
|
||||
handler_config["console"] = error_console
|
||||
elif handler_config.get("class") == "logging.handlers.RotatingFileHandler":
|
||||
logfile_path = Path(handler_config["filename"])
|
||||
try:
|
||||
logfile_path = Path(logfile)
|
||||
# Create parent for filehandler
|
||||
logfile_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
handler_rf = RotatingFileHandler(
|
||||
logfile_path,
|
||||
maxBytes=1024 * 1024 * 10, # 10Mb
|
||||
backupCount=10,
|
||||
)
|
||||
except PermissionError:
|
||||
raise OperationalException(
|
||||
f'Failed to create or access log file "{logfile_path.absolute()}". '
|
||||
@@ -122,10 +198,34 @@ def setup_logging(config: Config) -> None:
|
||||
"non-root user, delete and recreate the directories you need, and then try "
|
||||
"again."
|
||||
)
|
||||
handler_rf.setFormatter(Formatter(LOGFORMAT))
|
||||
logging.root.addHandler(handler_rf)
|
||||
return log_config
|
||||
|
||||
|
||||
def setup_logging(config: Config) -> None:
|
||||
"""
|
||||
Process -v/--verbose, --logfile options
|
||||
"""
|
||||
verbosity = config["verbosity"]
|
||||
if os.environ.get("PYTEST_VERSION") is None or config.get("ft_tests_force_logging"):
|
||||
log_config = _create_log_config(config)
|
||||
_set_log_levels(
|
||||
log_config, verbosity, config.get("api_server", {}).get("verbosity", "info")
|
||||
)
|
||||
|
||||
logging.config.dictConfig(log_config)
|
||||
|
||||
# Add buffer handler to root logger
|
||||
if bufferHandler not in logging.root.handlers:
|
||||
logging.root.addHandler(bufferHandler)
|
||||
|
||||
# Set color system for console output
|
||||
if config.get("print_colorized", True):
|
||||
logger.info("Enabling colorized output.")
|
||||
error_console._color_system = error_console._detect_color_system()
|
||||
|
||||
logging.info("Logfile configured")
|
||||
|
||||
# Set verbosity levels
|
||||
logging.root.setLevel(logging.INFO if verbosity < 1 else logging.DEBUG)
|
||||
set_loggers(verbosity, config.get("api_server", {}).get("verbosity", "info"))
|
||||
|
||||
logger.info("Verbosity set to %s", verbosity)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
"""
|
||||
Formatter that outputs JSON strings after parsing the LogRecord.
|
||||
|
||||
@param dict fmt_dict: Key: logging format attribute pairs. Defaults to {"message": "message"}.
|
||||
@param str time_format: time.strftime() format string. Default: "%Y-%m-%dT%H:%M:%S"
|
||||
@param str msec_format: Microsecond formatting. Appended at the end. Default: "%s.%03dZ"
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fmt_dict: dict | None = None,
|
||||
time_format: str = "%Y-%m-%dT%H:%M:%S",
|
||||
msec_format: str = "%s.%03dZ",
|
||||
):
|
||||
self.fmt_dict = (
|
||||
fmt_dict
|
||||
if fmt_dict is not None
|
||||
else {
|
||||
"timestamp": "asctime",
|
||||
"level": "levelname",
|
||||
"logger": "name",
|
||||
"message": "message",
|
||||
}
|
||||
)
|
||||
self.default_time_format = time_format
|
||||
self.default_msec_format = msec_format
|
||||
self.datefmt = None
|
||||
|
||||
def usesTime(self) -> bool:
|
||||
"""
|
||||
Look for the attribute in the format dict values instead of the fmt string.
|
||||
"""
|
||||
return "asctime" in self.fmt_dict.values()
|
||||
|
||||
def formatMessage(self, record) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
def formatMessageDict(self, record) -> dict:
|
||||
"""
|
||||
Return a dictionary of the relevant LogRecord attributes instead of a string.
|
||||
KeyError is raised if an unknown attribute is provided in the fmt_dict.
|
||||
"""
|
||||
return {fmt_key: record.__dict__[fmt_val] for fmt_key, fmt_val in self.fmt_dict.items()}
|
||||
|
||||
def format(self, record) -> str:
|
||||
"""
|
||||
Mostly the same as the parent's class method, the difference being that a dict is
|
||||
manipulated and dumped as JSON instead of a string.
|
||||
"""
|
||||
record.message = record.getMessage()
|
||||
|
||||
if self.usesTime():
|
||||
record.asctime = self.formatTime(record, self.datefmt)
|
||||
|
||||
message_dict = self.formatMessageDict(record)
|
||||
|
||||
if record.exc_info:
|
||||
# Cache the traceback text to avoid converting it multiple times
|
||||
# (it's constant anyway)
|
||||
if not record.exc_text:
|
||||
record.exc_text = self.formatException(record.exc_info)
|
||||
|
||||
if record.exc_text:
|
||||
message_dict["exc_info"] = record.exc_text
|
||||
|
||||
if record.stack_info:
|
||||
message_dict["stack_info"] = self.formatStack(record.stack_info)
|
||||
|
||||
return json.dumps(message_dict, default=str)
|
||||
@@ -0,0 +1,26 @@
|
||||
import sys
|
||||
from shutil import get_terminal_size
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
|
||||
def console_width() -> int | None:
|
||||
"""
|
||||
Get the width of the console
|
||||
"""
|
||||
if any(module in ["pytest", "ipykernel"] for module in sys.modules):
|
||||
return 200
|
||||
|
||||
width, _ = get_terminal_size((1, 24))
|
||||
# Fall back to 200 if terminal size is not available.
|
||||
# This is determined by assuming an insane width of 1char, which is unlikely.
|
||||
w = None if width > 1 else 200
|
||||
return w
|
||||
|
||||
|
||||
def get_rich_console(**kwargs) -> Console:
|
||||
"""
|
||||
Get a rich console with default settings
|
||||
"""
|
||||
kwargs["width"] = kwargs.get("width", console_width())
|
||||
return Console(**kwargs)
|
||||
@@ -4,25 +4,6 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def set_loggers(verbosity: int = 0, api_verbosity: str = "info") -> None:
|
||||
"""
|
||||
Set the logging level for third party libraries
|
||||
:param verbosity: Verbosity level. amount of `-v` passed to the command line
|
||||
:return: None
|
||||
"""
|
||||
for logger_name in ("requests", "urllib3", "httpcore"):
|
||||
logging.getLogger(logger_name).setLevel(logging.INFO if verbosity <= 1 else logging.DEBUG)
|
||||
logging.getLogger("ccxt.base.exchange").setLevel(
|
||||
logging.INFO if verbosity <= 2 else logging.DEBUG
|
||||
)
|
||||
logging.getLogger("telegram").setLevel(logging.INFO)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
|
||||
logging.getLogger("werkzeug").setLevel(
|
||||
logging.ERROR if api_verbosity == "error" else logging.INFO
|
||||
)
|
||||
|
||||
|
||||
__BIAS_TESTER_LOGGERS = [
|
||||
"freqtrade.resolvers",
|
||||
"freqtrade.strategy.hyper",
|
||||
|
||||
@@ -7,7 +7,7 @@ This module contains the backtesting logic
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from numpy import nan
|
||||
@@ -63,7 +63,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.util import FtPrecise
|
||||
from freqtrade.util import FtPrecise, dt_now
|
||||
from freqtrade.util.migrations import migrate_data
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
@@ -396,6 +396,8 @@ class Backtesting:
|
||||
self.canceled_trade_entries = 0
|
||||
self.canceled_entry_orders = 0
|
||||
self.replaced_entry_orders = 0
|
||||
self.canceled_exit_orders = 0
|
||||
self.replaced_exit_orders = 0
|
||||
self.dataprovider.clear_cache()
|
||||
if enable_protections:
|
||||
self._load_protections(self.strategy)
|
||||
@@ -601,7 +603,7 @@ class Backtesting:
|
||||
# This should not be reached...
|
||||
return row[OPEN_IDX]
|
||||
|
||||
def _get_adjust_trade_entry_for_candle(
|
||||
def _check_adjust_trade_for_candle(
|
||||
self, trade: LocalTrade, row: tuple, current_time: datetime
|
||||
) -> LocalTrade:
|
||||
current_rate: float = row[OPEN_IDX]
|
||||
@@ -712,7 +714,7 @@ class Backtesting:
|
||||
exchange=self.exchange,
|
||||
wallets=self.wallets,
|
||||
stake_currency=self.config["stake_currency"],
|
||||
dry_run=self.config["dry_run"],
|
||||
dry_run=True,
|
||||
)
|
||||
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount):
|
||||
self._call_adjust_stop(current_date, trade, order.ft_price)
|
||||
@@ -869,7 +871,7 @@ class Backtesting:
|
||||
|
||||
# Check if we need to adjust our current positions
|
||||
if self.strategy.position_adjustment_enable:
|
||||
trade = self._get_adjust_trade_entry_for_candle(trade, row, current_time)
|
||||
trade = self._check_adjust_trade_for_candle(trade, row, current_time)
|
||||
|
||||
if trade.is_open:
|
||||
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
||||
@@ -1234,8 +1236,8 @@ class Backtesting:
|
||||
for order in [o for o in trade.orders if o.ft_is_open]:
|
||||
if order.side == trade.entry_side:
|
||||
self.canceled_entry_orders += 1
|
||||
# elif order.side == trade.exit_side:
|
||||
# self.canceled_exit_orders += 1
|
||||
elif order.side == trade.exit_side:
|
||||
self.canceled_exit_orders += 1
|
||||
# canceled orders are removed from the trade
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
|
||||
@@ -1299,9 +1301,10 @@ class Backtesting:
|
||||
Returns True if the trade should be deleted.
|
||||
"""
|
||||
# only check on new candles for open entry orders
|
||||
if order.side == trade.entry_side and current_time > order.order_date_utc:
|
||||
if current_time > order.order_date_utc:
|
||||
is_entry = order.side == trade.entry_side
|
||||
requested_rate = strategy_safe_wrapper(
|
||||
self.strategy.adjust_entry_price, default_retval=order.ft_price
|
||||
self.strategy.adjust_order_price, default_retval=order.ft_price
|
||||
)(
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
order=order,
|
||||
@@ -1311,6 +1314,7 @@ class Backtesting:
|
||||
current_order_rate=order.ft_price,
|
||||
entry_tag=trade.enter_tag,
|
||||
side=trade.trade_direction,
|
||||
is_entry=is_entry,
|
||||
) # default value is current order price
|
||||
|
||||
# cancel existing order whenever a new rate is requested (or None)
|
||||
@@ -1319,22 +1323,35 @@ class Backtesting:
|
||||
return False
|
||||
else:
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
self.canceled_entry_orders += 1
|
||||
if is_entry:
|
||||
self.canceled_entry_orders += 1
|
||||
else:
|
||||
self.canceled_exit_orders += 1
|
||||
|
||||
# place new order if result was not None
|
||||
if requested_rate:
|
||||
self._enter_trade(
|
||||
pair=trade.pair,
|
||||
row=row,
|
||||
trade=trade,
|
||||
requested_rate=requested_rate,
|
||||
requested_stake=(order.safe_remaining * order.ft_price / trade.leverage),
|
||||
direction="short" if trade.is_short else "long",
|
||||
)
|
||||
if is_entry:
|
||||
self._enter_trade(
|
||||
pair=trade.pair,
|
||||
row=row,
|
||||
trade=trade,
|
||||
requested_rate=requested_rate,
|
||||
requested_stake=(order.safe_remaining * order.ft_price / trade.leverage),
|
||||
direction="short" if trade.is_short else "long",
|
||||
)
|
||||
self.replaced_entry_orders += 1
|
||||
else:
|
||||
self._exit_trade(
|
||||
trade=trade,
|
||||
sell_row=row,
|
||||
close_rate=requested_rate,
|
||||
amount=order.safe_remaining,
|
||||
exit_reason=order.ft_order_tag,
|
||||
)
|
||||
self.replaced_exit_orders += 1
|
||||
# Delete trade if no successful entries happened (if placing the new order failed)
|
||||
if not trade.has_open_orders and trade.nr_of_successful_entries == 0:
|
||||
if not trade.has_open_orders and is_entry and trade.nr_of_successful_entries == 0:
|
||||
return True
|
||||
self.replaced_entry_orders += 1
|
||||
else:
|
||||
# assumption: there can't be multiple open entry orders at any given time
|
||||
return trade.nr_of_successful_entries == 0
|
||||
@@ -1535,7 +1552,9 @@ class Backtesting:
|
||||
row_index += 1
|
||||
indexes[pair] = row_index
|
||||
is_last_row = current_time == end_date
|
||||
self.dataprovider._set_dataframe_max_index(self.required_startup + row_index)
|
||||
self.dataprovider._set_dataframe_max_index(
|
||||
pair, self.required_startup + row_index
|
||||
)
|
||||
trade_dir = self.check_for_trade_entry(row)
|
||||
pair_tradedir_cache[pair] = trade_dir
|
||||
|
||||
@@ -1656,7 +1675,7 @@ class Backtesting:
|
||||
self.progress.init_step(BacktestState.ANALYZE, 0)
|
||||
strategy_name = strat.get_strategy_name()
|
||||
logger.info(f"Running backtesting for Strategy {strategy_name}")
|
||||
backtest_start_time = datetime.now(timezone.utc)
|
||||
backtest_start_time = dt_now()
|
||||
self._set_strategy(strat)
|
||||
|
||||
# need to reprocess data every time to populate signals
|
||||
@@ -1683,7 +1702,7 @@ class Backtesting:
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
)
|
||||
backtest_end_time = datetime.now(timezone.utc)
|
||||
backtest_end_time = dt_now()
|
||||
results.update(
|
||||
{
|
||||
"run_id": self.run_ids.get(strategy_name, ""),
|
||||
@@ -1710,14 +1729,14 @@ class Backtesting:
|
||||
def _get_min_cached_backtest_date(self):
|
||||
min_backtest_date = None
|
||||
backtest_cache_age = self.config.get("backtest_cache", constants.BACKTEST_CACHE_DEFAULT)
|
||||
if self.timerange.stopts == 0 or self.timerange.stopdt > datetime.now(tz=timezone.utc):
|
||||
if self.timerange.stopts == 0 or self.timerange.stopdt > dt_now():
|
||||
logger.warning("Backtest result caching disabled due to use of open-ended timerange.")
|
||||
elif backtest_cache_age == "day":
|
||||
min_backtest_date = datetime.now(tz=timezone.utc) - timedelta(days=1)
|
||||
min_backtest_date = dt_now() - timedelta(days=1)
|
||||
elif backtest_cache_age == "week":
|
||||
min_backtest_date = datetime.now(tz=timezone.utc) - timedelta(weeks=1)
|
||||
min_backtest_date = dt_now() - timedelta(weeks=1)
|
||||
elif backtest_cache_age == "month":
|
||||
min_backtest_date = datetime.now(tz=timezone.utc) - timedelta(weeks=4)
|
||||
min_backtest_date = dt_now() - timedelta(weeks=4)
|
||||
return min_backtest_date
|
||||
|
||||
def load_prior_backtest(self):
|
||||
@@ -1773,6 +1792,7 @@ class Backtesting:
|
||||
dt_appendix,
|
||||
market_change_data=combined_res,
|
||||
analysis_results=self.analysis_results,
|
||||
strategy_files={s.get_strategy_name(): s.__file__ for s in self.strategylist},
|
||||
)
|
||||
|
||||
# Results may be mixed up now. Sort them so they follow --strategy-list order.
|
||||
|
||||
@@ -6,7 +6,6 @@ This module contains the hyperopt logic
|
||||
|
||||
import logging
|
||||
import random
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from math import ceil
|
||||
from multiprocessing import Manager
|
||||
@@ -15,7 +14,6 @@ from typing import Any
|
||||
|
||||
import rapidjson
|
||||
from joblib import Parallel, cpu_count, delayed, wrap_non_picklable_objects
|
||||
from joblib.externals import cloudpickle
|
||||
|
||||
from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config
|
||||
from freqtrade.enums import HyperoptState
|
||||
@@ -110,17 +108,6 @@ class Hyperopt:
|
||||
logger.info(f"Removing `{p}`.")
|
||||
p.unlink()
|
||||
|
||||
def hyperopt_pickle_magic(self, bases) -> None:
|
||||
"""
|
||||
Hyperopt magic to allow strategy inheritance across files.
|
||||
For this to properly work, we need to register the module of the imported class
|
||||
to pickle as value.
|
||||
"""
|
||||
for modules in bases:
|
||||
if modules.__name__ != "IStrategy":
|
||||
cloudpickle.register_pickle_by_value(sys.modules[modules.__module__])
|
||||
self.hyperopt_pickle_magic(modules.__bases__)
|
||||
|
||||
def _save_result(self, epoch: dict) -> None:
|
||||
"""
|
||||
Save hyperopt results to file
|
||||
|
||||
@@ -114,7 +114,7 @@ class HyperOptimizer:
|
||||
def get_strategy_name(self) -> str:
|
||||
return self.backtesting.strategy.get_strategy_name()
|
||||
|
||||
def hyperopt_pickle_magic(self, bases) -> None:
|
||||
def hyperopt_pickle_magic(self, bases: tuple[type, ...]) -> None:
|
||||
"""
|
||||
Hyperopt magic to allow strategy inheritance across files.
|
||||
For this to properly work, we need to register the module of the imported class
|
||||
@@ -122,7 +122,8 @@ class HyperOptimizer:
|
||||
"""
|
||||
for modules in bases:
|
||||
if modules.__name__ != "IStrategy":
|
||||
cloudpickle.register_pickle_by_value(sys.modules[modules.__module__])
|
||||
if mod := sys.modules.get(modules.__module__):
|
||||
cloudpickle.register_pickle_by_value(mod)
|
||||
self.hyperopt_pickle_magic(modules.__bases__)
|
||||
|
||||
def _get_params_dict(
|
||||
|
||||
@@ -132,18 +132,18 @@ def text_table_periodic_breakdown(
|
||||
"""
|
||||
headers = [
|
||||
period.capitalize(),
|
||||
"Trades",
|
||||
f"Tot Profit {stake_currency}",
|
||||
"Wins",
|
||||
"Draws",
|
||||
"Losses",
|
||||
"Profit Factor",
|
||||
"Win Draw Loss Win%",
|
||||
]
|
||||
output = [
|
||||
[
|
||||
d["date"],
|
||||
d.get("trades", "N/A"),
|
||||
fmt_coin(d["profit_abs"], stake_currency, False),
|
||||
d["wins"],
|
||||
d["draws"],
|
||||
d["loses"],
|
||||
round(d["profit_factor"], 2) if "profit_factor" in d else "N/A",
|
||||
generate_wins_draws_losses(d["wins"], d["draws"], d.get("losses", d.get("loses", 0))),
|
||||
]
|
||||
for d in days_breakdown_stats
|
||||
]
|
||||
@@ -312,6 +312,7 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
||||
("Sortino", f"{strat_results['sortino']:.2f}" if "sortino" in strat_results else "N/A"),
|
||||
("Sharpe", f"{strat_results['sharpe']:.2f}" if "sharpe" in strat_results else "N/A"),
|
||||
("Calmar", f"{strat_results['calmar']:.2f}" if "calmar" in strat_results else "N/A"),
|
||||
("SQN", f"{strat_results['sqn']:.2f}" if "sqn" in strat_results else "N/A"),
|
||||
(
|
||||
"Profit factor",
|
||||
(
|
||||
|
||||
@@ -6,6 +6,7 @@ from zipfile import ZIP_DEFLATED, ZipFile
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import sanitize_config
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||
from freqtrade.enums.runmode import RunMode
|
||||
from freqtrade.ft_types import BacktestResultType
|
||||
@@ -52,6 +53,7 @@ def store_backtest_results(
|
||||
*,
|
||||
market_change_data: DataFrame | None = None,
|
||||
analysis_results: dict[str, dict[str, DataFrame]] | None = None,
|
||||
strategy_files: dict[str, str] | None = None,
|
||||
) -> Path:
|
||||
"""
|
||||
Stores backtest results and analysis data in a zip file, with metadata stored separately
|
||||
@@ -85,6 +87,32 @@ def store_backtest_results(
|
||||
dump_json_to_file(stats_buf, stats_copy)
|
||||
zipf.writestr(json_filename.name, stats_buf.getvalue())
|
||||
|
||||
config_buf = StringIO()
|
||||
dump_json_to_file(config_buf, sanitize_config(config["original_config"]))
|
||||
zipf.writestr(f"{base_filename.stem}_config.json", config_buf.getvalue())
|
||||
|
||||
for strategy_name, strategy_file in (strategy_files or {}).items():
|
||||
# Store the strategy file and its parameters
|
||||
strategy_buf = BytesIO()
|
||||
strategy_path = Path(strategy_file)
|
||||
if not strategy_path.is_file():
|
||||
logger.warning(f"Strategy file '{strategy_path}' does not exist. Skipping.")
|
||||
continue
|
||||
with strategy_path.open("rb") as strategy_file_obj:
|
||||
strategy_buf.write(strategy_file_obj.read())
|
||||
strategy_buf.seek(0)
|
||||
zipf.writestr(f"{base_filename.stem}_{strategy_name}.py", strategy_buf.getvalue())
|
||||
strategy_params = strategy_path.with_suffix(".json")
|
||||
if strategy_params.is_file():
|
||||
strategy_params_buf = BytesIO()
|
||||
with strategy_params.open("rb") as strategy_params_obj:
|
||||
strategy_params_buf.write(strategy_params_obj.read())
|
||||
strategy_params_buf.seek(0)
|
||||
zipf.writestr(
|
||||
f"{base_filename.stem}_{strategy_name}.json",
|
||||
strategy_params_buf.getvalue(),
|
||||
)
|
||||
|
||||
# Add market change data if present
|
||||
if market_change_data is not None:
|
||||
market_change_name = f"{base_filename.stem}_market_change.feather"
|
||||
|
||||
@@ -16,8 +16,9 @@ from freqtrade.data.metrics import (
|
||||
calculate_max_drawdown,
|
||||
calculate_sharpe,
|
||||
calculate_sortino,
|
||||
calculate_sqn,
|
||||
)
|
||||
from freqtrade.ft_types import BacktestResultType
|
||||
from freqtrade.ft_types import BacktestResultType, get_BacktestResultType_default
|
||||
from freqtrade.util import decimals_per_coin, fmt_coin, get_dry_run_wallet
|
||||
|
||||
|
||||
@@ -211,6 +212,8 @@ def _get_resample_from_period(period: str) -> str:
|
||||
return "1W-MON"
|
||||
if period == "month":
|
||||
return "1ME"
|
||||
if period == "year":
|
||||
return "1YE"
|
||||
raise ValueError(f"Period {period} is not supported.")
|
||||
|
||||
|
||||
@@ -228,8 +231,11 @@ def generate_periodic_breakdown_stats(
|
||||
profit_abs = day["profit_abs"].sum().round(10)
|
||||
wins = sum(day["profit_abs"] > 0)
|
||||
draws = sum(day["profit_abs"] == 0)
|
||||
loses = sum(day["profit_abs"] < 0)
|
||||
trades = wins + draws + loses
|
||||
losses = sum(day["profit_abs"] < 0)
|
||||
trades = wins + draws + losses
|
||||
winning_profit = day.loc[day["profit_abs"] > 0, "profit_abs"].sum()
|
||||
losing_profit = day.loc[day["profit_abs"] < 0, "profit_abs"].sum()
|
||||
profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0
|
||||
stats.append(
|
||||
{
|
||||
"date": name.strftime("%d/%m/%Y"),
|
||||
@@ -237,8 +243,9 @@ def generate_periodic_breakdown_stats(
|
||||
"profit_abs": profit_abs,
|
||||
"wins": wins,
|
||||
"draws": draws,
|
||||
"loses": loses,
|
||||
"winrate": wins / trades if trades else 0.0,
|
||||
"losses": losses,
|
||||
"trades": trades,
|
||||
"profit_factor": round(profit_factor, 8),
|
||||
}
|
||||
)
|
||||
return stats
|
||||
@@ -468,6 +475,7 @@ def generate_strategy_stats(
|
||||
"sortino": calculate_sortino(results, min_date, max_date, start_balance),
|
||||
"sharpe": calculate_sharpe(results, min_date, max_date, start_balance),
|
||||
"calmar": calculate_calmar(results, min_date, max_date, start_balance),
|
||||
"sqn": calculate_sqn(results, start_balance),
|
||||
"profit_factor": profit_factor,
|
||||
"backtest_start": min_date.strftime(DATETIME_PRINT_FORMAT),
|
||||
"backtest_start_ts": int(min_date.timestamp() * 1000),
|
||||
@@ -579,11 +587,7 @@ def generate_backtest_stats(
|
||||
:param max_date: Backtest end date
|
||||
:return: Dictionary containing results per strategy and a strategy summary.
|
||||
"""
|
||||
result: BacktestResultType = {
|
||||
"metadata": {},
|
||||
"strategy": {},
|
||||
"strategy_comparison": [],
|
||||
}
|
||||
result: BacktestResultType = get_BacktestResultType_default()
|
||||
market_change = calculate_market_change(btdata, "close")
|
||||
metadata = {}
|
||||
pairlist = list(btdata.keys())
|
||||
|
||||
@@ -124,6 +124,7 @@ def migrate_trades_and_orders_table(
|
||||
funding_fees = get_column_def(cols, "funding_fees", "0.0")
|
||||
funding_fee_running = get_column_def(cols, "funding_fee_running", "null")
|
||||
max_stake_amount = get_column_def(cols, "max_stake_amount", "stake_amount")
|
||||
record_version = get_column_def(cols, "record_version", "1")
|
||||
|
||||
# If ticker-interval existed use that, else null.
|
||||
if has_column(cols, "ticker_interval"):
|
||||
@@ -180,7 +181,7 @@ def migrate_trades_and_orders_table(
|
||||
trading_mode, leverage, liquidation_price, is_short,
|
||||
interest_rate, funding_fees, funding_fee_running, realized_profit,
|
||||
amount_precision, price_precision, precision_mode, precision_mode_price, contract_size,
|
||||
max_stake_amount
|
||||
max_stake_amount, record_version
|
||||
)
|
||||
select id, lower(exchange), pair, {base_currency} base_currency,
|
||||
{stake_currency} stake_currency,
|
||||
@@ -210,7 +211,8 @@ def migrate_trades_and_orders_table(
|
||||
{realized_profit} realized_profit,
|
||||
{amount_precision} amount_precision, {price_precision} price_precision,
|
||||
{precision_mode} precision_mode, {precision_mode_price} precision_mode_price,
|
||||
{contract_size} contract_size, {max_stake_amount} max_stake_amount
|
||||
{contract_size} contract_size, {max_stake_amount} max_stake_amount,
|
||||
{record_version} record_version
|
||||
from {trade_back_name}
|
||||
"""
|
||||
)
|
||||
@@ -329,6 +331,25 @@ def fix_old_dry_orders(engine):
|
||||
connection.execute(stmt)
|
||||
|
||||
|
||||
def fix_wrong_max_stake_amount(engine):
|
||||
"""
|
||||
Fix max_stake_amount for leveraged closed trades
|
||||
This caused record_version to be bumped to 2.
|
||||
"""
|
||||
with engine.begin() as connection:
|
||||
stmt = (
|
||||
update(Trade)
|
||||
.where(
|
||||
Trade.record_version < 2,
|
||||
Trade.leverage > 1,
|
||||
Trade.is_open.is_(False),
|
||||
Trade.max_stake_amount != 0,
|
||||
)
|
||||
.values(max_stake_amount=Trade.max_stake_amount / Trade.leverage, record_version=2)
|
||||
)
|
||||
connection.execute(stmt)
|
||||
|
||||
|
||||
def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
"""
|
||||
Checks if migration is necessary and migrates if necessary
|
||||
@@ -350,7 +371,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
# if ('orders' not in previous_tables
|
||||
# or not has_column(cols_orders, 'funding_fee')):
|
||||
migrating = False
|
||||
if not has_column(cols_trades, "precision_mode_price"):
|
||||
if not has_column(cols_trades, "record_version"):
|
||||
# if not has_column(cols_orders, "ft_order_tag"):
|
||||
migrating = True
|
||||
logger.info(
|
||||
@@ -383,6 +404,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
|
||||
set_sqlite_to_wal(engine)
|
||||
fix_old_dry_orders(engine)
|
||||
fix_wrong_max_stake_amount(engine)
|
||||
|
||||
if migrating:
|
||||
logger.info("Database migration finished.")
|
||||
|
||||
@@ -464,6 +464,8 @@ class LocalTrade:
|
||||
# Used to keep running funding fees - between the last filled order and now
|
||||
# Shall not be used for calculations!
|
||||
funding_fee_running: float | None = None
|
||||
# v 2 -> correct max_stake_amount calculation for leveraged trades
|
||||
record_version: int = 2
|
||||
|
||||
@property
|
||||
def stoploss_or_liquidation(self) -> float:
|
||||
@@ -1243,7 +1245,7 @@ class LocalTrade:
|
||||
total_stake += self._calc_open_trade_value(tmp_amount, price)
|
||||
max_stake_amount += tmp_amount * price
|
||||
self.funding_fees = funding_fees
|
||||
self.max_stake_amount = float(max_stake_amount)
|
||||
self.max_stake_amount = float(max_stake_amount) / (self.leverage or 1.0)
|
||||
|
||||
if close_profit:
|
||||
self.close_profit = close_profit
|
||||
@@ -1351,8 +1353,10 @@ class LocalTrade:
|
||||
|
||||
def get_custom_data(self, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Get custom data for this trade
|
||||
Get custom data for this trade.
|
||||
|
||||
:param key: key of the custom data
|
||||
:param default: value to return if no data is found
|
||||
"""
|
||||
data = CustomDataWrapper.get_custom_data(trade_id=self.id, key=key)
|
||||
if data:
|
||||
@@ -1535,45 +1539,47 @@ class LocalTrade:
|
||||
:param json_str: json string to parse
|
||||
:return: Trade instance
|
||||
"""
|
||||
from uuid import uuid4
|
||||
|
||||
import rapidjson
|
||||
|
||||
data = rapidjson.loads(json_str)
|
||||
trade = cls(
|
||||
__FROM_JSON=True,
|
||||
id=data["trade_id"],
|
||||
id=data.get("trade_id"),
|
||||
pair=data["pair"],
|
||||
base_currency=data["base_currency"],
|
||||
stake_currency=data["quote_currency"],
|
||||
base_currency=data.get("base_currency"),
|
||||
stake_currency=data.get("quote_currency"),
|
||||
is_open=data["is_open"],
|
||||
exchange=data["exchange"],
|
||||
exchange=data.get("exchange", "import"),
|
||||
amount=data["amount"],
|
||||
amount_requested=data["amount_requested"],
|
||||
amount_requested=data.get("amount_requested", data["amount"]),
|
||||
stake_amount=data["stake_amount"],
|
||||
strategy=data["strategy"],
|
||||
strategy=data.get("strategy"),
|
||||
enter_tag=data["enter_tag"],
|
||||
timeframe=data["timeframe"],
|
||||
timeframe=data.get("timeframe"),
|
||||
fee_open=data["fee_open"],
|
||||
fee_open_cost=data["fee_open_cost"],
|
||||
fee_open_currency=data["fee_open_currency"],
|
||||
fee_open_cost=data.get("fee_open_cost"),
|
||||
fee_open_currency=data.get("fee_open_currency"),
|
||||
fee_close=data["fee_close"],
|
||||
fee_close_cost=data["fee_close_cost"],
|
||||
fee_close_currency=data["fee_close_currency"],
|
||||
fee_close_cost=data.get("fee_close_cost"),
|
||||
fee_close_currency=data.get("fee_close_currency"),
|
||||
open_date=datetime.fromtimestamp(data["open_timestamp"] // 1000, tz=timezone.utc),
|
||||
open_rate=data["open_rate"],
|
||||
open_rate_requested=data["open_rate_requested"],
|
||||
open_trade_value=data["open_trade_value"],
|
||||
open_rate_requested=data.get("open_rate_requested", data["open_rate"]),
|
||||
open_trade_value=data.get("open_trade_value"),
|
||||
close_date=(
|
||||
datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc)
|
||||
if data["close_timestamp"]
|
||||
else None
|
||||
),
|
||||
realized_profit=data["realized_profit"],
|
||||
realized_profit=data.get("realized_profit", 0),
|
||||
close_rate=data["close_rate"],
|
||||
close_rate_requested=data["close_rate_requested"],
|
||||
close_profit=data["close_profit"],
|
||||
close_profit_abs=data["close_profit_abs"],
|
||||
close_rate_requested=data.get("close_rate_requested", data["close_rate"]),
|
||||
close_profit=data.get("close_profit", data.get("profit_ratio")),
|
||||
close_profit_abs=data.get("close_profit_abs", data.get("profit_abs")),
|
||||
exit_reason=data["exit_reason"],
|
||||
exit_order_status=data["exit_order_status"],
|
||||
exit_order_status=data.get("exit_order_status"),
|
||||
stop_loss=data["stop_loss_abs"],
|
||||
stop_loss_pct=data["stop_loss_ratio"],
|
||||
initial_stop_loss=data["initial_stop_loss_abs"],
|
||||
@@ -1581,11 +1587,11 @@ class LocalTrade:
|
||||
min_rate=data["min_rate"],
|
||||
max_rate=data["max_rate"],
|
||||
leverage=data["leverage"],
|
||||
interest_rate=data["interest_rate"],
|
||||
liquidation_price=data["liquidation_price"],
|
||||
interest_rate=data.get("interest_rate"),
|
||||
liquidation_price=data.get("liquidation_price"),
|
||||
is_short=data["is_short"],
|
||||
trading_mode=data["trading_mode"],
|
||||
funding_fees=data["funding_fees"],
|
||||
trading_mode=data.get("trading_mode"),
|
||||
funding_fees=data.get("funding_fees"),
|
||||
amount_precision=data.get("amount_precision", None),
|
||||
price_precision=data.get("price_precision", None),
|
||||
precision_mode=data.get("precision_mode", None),
|
||||
@@ -1597,23 +1603,25 @@ class LocalTrade:
|
||||
amount=order["amount"],
|
||||
ft_amount=order["amount"],
|
||||
ft_order_side=order["ft_order_side"],
|
||||
ft_pair=order["pair"],
|
||||
ft_is_open=order["is_open"],
|
||||
order_id=order["order_id"],
|
||||
status=order["status"],
|
||||
average=order["average"],
|
||||
ft_pair=order.get("pair", data["pair"]),
|
||||
ft_is_open=order.get("is_open", False),
|
||||
order_id=order.get("order_id", uuid4().hex),
|
||||
status=order.get("status"),
|
||||
average=order.get("average", order.get("safe_price")),
|
||||
cost=order["cost"],
|
||||
filled=order["filled"],
|
||||
order_date=datetime.strptime(order["order_date"], DATETIME_PRINT_FORMAT),
|
||||
filled=order.get("filled", order["amount"]),
|
||||
order_date=datetime.strptime(order["order_date"], DATETIME_PRINT_FORMAT)
|
||||
if order.get("order_date")
|
||||
else None,
|
||||
order_filled_date=(
|
||||
datetime.fromtimestamp(order["order_filled_timestamp"] // 1000, tz=timezone.utc)
|
||||
if order["order_filled_timestamp"]
|
||||
else None
|
||||
),
|
||||
order_type=order["order_type"],
|
||||
price=order["price"],
|
||||
ft_price=order["price"],
|
||||
remaining=order["remaining"],
|
||||
order_type=order.get("order_type"),
|
||||
price=order.get("price", order.get("safe_price")),
|
||||
ft_price=order.get("price", order.get("safe_price")),
|
||||
remaining=order.get("remaining", 0.0),
|
||||
funding_fee=order.get("funding_fee", None),
|
||||
ft_order_tag=order.get("ft_order_tag", None),
|
||||
)
|
||||
@@ -1748,6 +1756,8 @@ class Trade(ModelBase, LocalTrade):
|
||||
Float(), nullable=True, default=None
|
||||
)
|
||||
|
||||
record_version: Mapped[int] = mapped_column(Integer, nullable=False, default=2) # type: ignore
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
from_json = kwargs.pop("__FROM_JSON", None)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import Any
|
||||
import freqtrade.exchange as exchanges
|
||||
from freqtrade.constants import Config, ExchangeConfig
|
||||
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, Exchange
|
||||
from freqtrade.resolvers import IResolver
|
||||
from freqtrade.resolvers.iresolver import IResolver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,7 +16,7 @@ from freqtrade.configuration.config_validation import validate_migrated_strategy
|
||||
from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES, Config
|
||||
from freqtrade.enums import TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.resolvers import IResolver
|
||||
from freqtrade.resolvers.iresolver import IResolver
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
||||
|
||||
@@ -242,6 +242,14 @@ class StrategyResolver(IResolver):
|
||||
if has_after_fill:
|
||||
strategy._ft_stop_uses_after_fill = True
|
||||
|
||||
if check_override(strategy, IStrategy, "adjust_order_price") and (
|
||||
check_override(strategy, IStrategy, "adjust_entry_price")
|
||||
or check_override(strategy, IStrategy, "adjust_exit_price")
|
||||
):
|
||||
raise OperationalException(
|
||||
"If you implement `adjust_order_price`, `adjust_entry_price` and "
|
||||
"`adjust_exit_price` will not be used. Please pick one approach for your strategy."
|
||||
)
|
||||
return strategy
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -99,16 +99,21 @@ def __run_backtest_bg(btconfig: Config):
|
||||
ApiBG.bt["data"], ApiBG.bt["bt"].all_results, min_date=min_date, max_date=max_date
|
||||
)
|
||||
|
||||
if btconfig.get("export", "none") == "trades":
|
||||
combined_res = combined_dataframes_with_rel_mean(ApiBG.bt["data"], min_date, max_date)
|
||||
fn = store_backtest_results(
|
||||
btconfig,
|
||||
ApiBG.bt["bt"].results,
|
||||
datetime.now().strftime("%Y-%m-%d_%H-%M-%S"),
|
||||
market_change_data=combined_res,
|
||||
)
|
||||
ApiBG.bt["bt"].results["metadata"][strategy_name]["filename"] = str(fn.stem)
|
||||
ApiBG.bt["bt"].results["metadata"][strategy_name]["strategy"] = strategy_name
|
||||
if btconfig.get("export", "none") == "trades":
|
||||
combined_res = combined_dataframes_with_rel_mean(
|
||||
ApiBG.bt["data"], min_date, max_date
|
||||
)
|
||||
fn = store_backtest_results(
|
||||
btconfig,
|
||||
ApiBG.bt["bt"].results,
|
||||
datetime.now().strftime("%Y-%m-%d_%H-%M-%S"),
|
||||
market_change_data=combined_res,
|
||||
strategy_files={
|
||||
s.get_strategy_name(): s.__file__ for s in ApiBG.bt["bt"].strategylist
|
||||
},
|
||||
)
|
||||
ApiBG.bt["bt"].results["metadata"][strategy_name]["filename"] = str(fn.stem)
|
||||
ApiBG.bt["bt"].results["metadata"][strategy_name]["strategy"] = strategy_name
|
||||
|
||||
logger.info("Backtest finished.")
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from freqtrade.configuration import validate_config_consistency
|
||||
from freqtrade.rpc.api_server.api_pairlists import handleExchangePayload
|
||||
from freqtrade.rpc.api_server.api_schemas import PairHistory, PairHistoryRequest
|
||||
from freqtrade.rpc.api_server.deps import get_config, get_exchange
|
||||
from freqtrade.rpc.rpc import RPC
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/pair_history", response_model=PairHistory, tags=["candle data"])
|
||||
def pair_history(
|
||||
pair: str,
|
||||
timeframe: str,
|
||||
timerange: str,
|
||||
strategy: str,
|
||||
freqaimodel: str | None = None,
|
||||
config=Depends(get_config),
|
||||
exchange=Depends(get_exchange),
|
||||
):
|
||||
# The initial call to this endpoint can be slow, as it may need to initialize
|
||||
# the exchange class.
|
||||
config_loc = deepcopy(config)
|
||||
config_loc.update(
|
||||
{
|
||||
"timeframe": timeframe,
|
||||
"strategy": strategy,
|
||||
"timerange": timerange,
|
||||
"freqaimodel": freqaimodel if freqaimodel else config_loc.get("freqaimodel"),
|
||||
}
|
||||
)
|
||||
validate_config_consistency(config_loc)
|
||||
try:
|
||||
return RPC._rpc_analysed_history_full(config_loc, pair, timeframe, exchange, None, False)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/pair_history", response_model=PairHistory, tags=["candle data"])
|
||||
def pair_history_filtered(payload: PairHistoryRequest, config=Depends(get_config)):
|
||||
# The initial call to this endpoint can be slow, as it may need to initialize
|
||||
# the exchange class.
|
||||
config_loc = deepcopy(config)
|
||||
config_loc.update(
|
||||
{
|
||||
"timeframe": payload.timeframe,
|
||||
"strategy": payload.strategy,
|
||||
"timerange": payload.timerange,
|
||||
"freqaimodel": (
|
||||
payload.freqaimodel if payload.freqaimodel else config_loc.get("freqaimodel")
|
||||
),
|
||||
}
|
||||
)
|
||||
handleExchangePayload(payload, config_loc)
|
||||
exchange = get_exchange(config_loc)
|
||||
|
||||
validate_config_consistency(config_loc)
|
||||
|
||||
try:
|
||||
return RPC._rpc_analysed_history_full(
|
||||
config_loc,
|
||||
payload.pair,
|
||||
payload.timeframe,
|
||||
exchange,
|
||||
payload.columns,
|
||||
payload.live_mode,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Error in pair_history_filtered")
|
||||
raise HTTPException(status_code=502, detail=str(e))
|
||||
@@ -110,13 +110,17 @@ def handleExchangePayload(payload: ExchangeModePayloadMixin, config_loc: Config)
|
||||
Handle exchange and trading mode payload.
|
||||
Updates the configuration with the payload values.
|
||||
"""
|
||||
from freqtrade.configuration.directory_operations import create_datadir
|
||||
|
||||
if payload.exchange:
|
||||
config_loc["exchange"]["name"] = payload.exchange
|
||||
config_loc.update({"datadir": create_datadir(config_loc, None)})
|
||||
if payload.trading_mode:
|
||||
config_loc["trading_mode"] = payload.trading_mode
|
||||
config_loc["candle_type_def"] = CandleType.get_default(
|
||||
config_loc.get("trading_mode", "spot") or "spot"
|
||||
)
|
||||
|
||||
if payload.margin_mode:
|
||||
config_loc["margin_mode"] = payload.margin_mode
|
||||
|
||||
|
||||
@@ -524,10 +524,11 @@ class PairCandlesRequest(BaseModel):
|
||||
columns: list[str] | None = None
|
||||
|
||||
|
||||
class PairHistoryRequest(PairCandlesRequest):
|
||||
class PairHistoryRequest(PairCandlesRequest, ExchangeModePayloadMixin):
|
||||
timerange: str
|
||||
strategy: str
|
||||
strategy: str | None = None
|
||||
freqaimodel: str | None = None
|
||||
live_mode: bool = False
|
||||
|
||||
|
||||
class PairHistory(BaseModel):
|
||||
@@ -606,6 +607,24 @@ class BacktestMarketChange(BaseModel):
|
||||
data: list[list[Any]]
|
||||
|
||||
|
||||
class MarketRequest(ExchangeModePayloadMixin, BaseModel):
|
||||
base: str | None = None
|
||||
quote: str | None = None
|
||||
|
||||
|
||||
class MarketModel(BaseModel):
|
||||
symbol: str
|
||||
base: str
|
||||
quote: str
|
||||
spot: bool
|
||||
swap: bool
|
||||
|
||||
|
||||
class MarketResponse(BaseModel):
|
||||
markets: dict[str, MarketModel]
|
||||
exchange_id: str
|
||||
|
||||
|
||||
class SysInfo(BaseModel):
|
||||
cpu_pct: list[float]
|
||||
ram_pct: float
|
||||
@@ -618,3 +637,16 @@ class Health(BaseModel):
|
||||
bot_start_ts: int | None = None
|
||||
bot_startup: datetime | None = None
|
||||
bot_startup_ts: int | None = None
|
||||
|
||||
|
||||
class CustomDataEntry(BaseModel):
|
||||
key: str
|
||||
type: str
|
||||
value: Any
|
||||
created_at: datetime
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class ListCustomData(BaseModel):
|
||||
trade_id: int
|
||||
custom_data: list[CustomDataEntry]
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.exceptions import HTTPException
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.data.history import get_datahandler
|
||||
from freqtrade.enums import CandleType, State, TradingMode
|
||||
from freqtrade.enums import CandleType, RunMode, State, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.rpc import RPC
|
||||
from freqtrade.rpc.api_server.api_pairlists import handleExchangePayload
|
||||
from freqtrade.rpc.api_server.api_schemas import (
|
||||
AvailablePairs,
|
||||
Balances,
|
||||
@@ -27,14 +29,16 @@ from freqtrade.rpc.api_server.api_schemas import (
|
||||
FreqAIModelListResponse,
|
||||
Health,
|
||||
HyperoptLossListResponse,
|
||||
ListCustomData,
|
||||
Locks,
|
||||
LocksPayload,
|
||||
Logs,
|
||||
MarketRequest,
|
||||
MarketResponse,
|
||||
MixTag,
|
||||
OpenTradeSchema,
|
||||
PairCandlesRequest,
|
||||
PairHistory,
|
||||
PairHistoryRequest,
|
||||
PerformanceEntry,
|
||||
Ping,
|
||||
PlotConfig,
|
||||
@@ -84,7 +88,8 @@ logger = logging.getLogger(__name__)
|
||||
# 2.35: pair_candles and pair_history endpoints as Post variant
|
||||
# 2.40: Add hyperopt-loss endpoint
|
||||
# 2.41: Add download-data endpoint
|
||||
API_VERSION = 2.41
|
||||
# 2.42: Add /pair_history endpoint with live data
|
||||
API_VERSION = 2.42
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
@@ -149,21 +154,33 @@ def stats(rpc: RPC = Depends(get_rpc)):
|
||||
|
||||
|
||||
@router.get("/daily", response_model=DailyWeeklyMonthly, tags=["info"])
|
||||
def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
def daily(
|
||||
timescale: int = Query(7, ge=1, description="Number of days to fetch data for"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
config=Depends(get_config),
|
||||
):
|
||||
return rpc._rpc_timeunit_profit(
|
||||
timescale, config["stake_currency"], config.get("fiat_display_currency", "")
|
||||
)
|
||||
|
||||
|
||||
@router.get("/weekly", response_model=DailyWeeklyMonthly, tags=["info"])
|
||||
def weekly(timescale: int = 4, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
def weekly(
|
||||
timescale: int = Query(4, ge=1, description="Number of weeks to fetch data for"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
config=Depends(get_config),
|
||||
):
|
||||
return rpc._rpc_timeunit_profit(
|
||||
timescale, config["stake_currency"], config.get("fiat_display_currency", ""), "weeks"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/monthly", response_model=DailyWeeklyMonthly, tags=["info"])
|
||||
def monthly(timescale: int = 3, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
def monthly(
|
||||
timescale: int = Query(3, ge=1, description="Number of months to fetch data for"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
config=Depends(get_config),
|
||||
):
|
||||
return rpc._rpc_timeunit_profit(
|
||||
timescale, config["stake_currency"], config.get("fiat_display_currency", ""), "months"
|
||||
)
|
||||
@@ -180,7 +197,11 @@ def status(rpc: RPC = Depends(get_rpc)):
|
||||
# Using the responsemodel here will cause a ~100% increase in response time (from 1s to 2s)
|
||||
# on big databases. Correct response model: response_model=TradeResponse,
|
||||
@router.get("/trades", tags=["info", "trading"])
|
||||
def trades(limit: int = 500, offset: int = 0, rpc: RPC = Depends(get_rpc)):
|
||||
def trades(
|
||||
limit: int = Query(500, ge=1, description="Maximum number of different trades to return data"),
|
||||
offset: int = Query(0, ge=0, description="Number of trades to skip for pagination"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
):
|
||||
return rpc._rpc_trade_history(limit, offset=offset, order_by_id=True)
|
||||
|
||||
|
||||
@@ -209,6 +230,36 @@ def trade_reload(tradeid: int, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_trade_status([tradeid])[0]
|
||||
|
||||
|
||||
@router.get("/trades/open/custom-data", response_model=list[ListCustomData], tags=["trading"])
|
||||
def list_open_trades_custom_data(
|
||||
key: str | None = Query(None, description="Optional key to filter data"),
|
||||
limit: int = Query(100, ge=1, description="Maximum number of different trades to return data"),
|
||||
offset: int = Query(0, ge=0, description="Number of trades to skip for pagination"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
):
|
||||
"""
|
||||
Fetch custom data for all open trades.
|
||||
If a key is provided, it will be used to filter data accordingly.
|
||||
Pagination is implemented via the `limit` and `offset` parameters.
|
||||
"""
|
||||
try:
|
||||
return rpc._rpc_list_custom_data(key=key, limit=limit, offset=offset)
|
||||
except RPCException as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/trades/{trade_id}/custom-data", response_model=list[ListCustomData], tags=["trading"])
|
||||
def list_custom_data(trade_id: int, key: str | None = Query(None), rpc: RPC = Depends(get_rpc)):
|
||||
"""
|
||||
Fetch custom data for a specific trade.
|
||||
If a key is provided, it will be used to filter data accordingly.
|
||||
"""
|
||||
try:
|
||||
return rpc._rpc_list_custom_data(trade_id, key=key)
|
||||
except RPCException as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
# TODO: Missing response model
|
||||
@router.get("/edge", tags=["info"])
|
||||
def edge(rpc: RPC = Depends(get_rpc)):
|
||||
@@ -342,58 +393,6 @@ def pair_candles_filtered(payload: PairCandlesRequest, rpc: RPC = Depends(get_rp
|
||||
)
|
||||
|
||||
|
||||
@router.get("/pair_history", response_model=PairHistory, tags=["candle data"])
|
||||
def pair_history(
|
||||
pair: str,
|
||||
timeframe: str,
|
||||
timerange: str,
|
||||
strategy: str,
|
||||
freqaimodel: str | None = None,
|
||||
config=Depends(get_config),
|
||||
exchange=Depends(get_exchange),
|
||||
):
|
||||
# The initial call to this endpoint can be slow, as it may need to initialize
|
||||
# the exchange class.
|
||||
config = deepcopy(config)
|
||||
config.update(
|
||||
{
|
||||
"timeframe": timeframe,
|
||||
"strategy": strategy,
|
||||
"timerange": timerange,
|
||||
"freqaimodel": freqaimodel if freqaimodel else config.get("freqaimodel"),
|
||||
}
|
||||
)
|
||||
try:
|
||||
return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange, None)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/pair_history", response_model=PairHistory, tags=["candle data"])
|
||||
def pair_history_filtered(
|
||||
payload: PairHistoryRequest, config=Depends(get_config), exchange=Depends(get_exchange)
|
||||
):
|
||||
# The initial call to this endpoint can be slow, as it may need to initialize
|
||||
# the exchange class.
|
||||
config = deepcopy(config)
|
||||
config.update(
|
||||
{
|
||||
"timeframe": payload.timeframe,
|
||||
"strategy": payload.strategy,
|
||||
"timerange": payload.timerange,
|
||||
"freqaimodel": (
|
||||
payload.freqaimodel if payload.freqaimodel else config.get("freqaimodel")
|
||||
),
|
||||
}
|
||||
)
|
||||
try:
|
||||
return RPC._rpc_analysed_history_full(
|
||||
config, payload.pair, payload.timeframe, exchange, payload.columns
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/plot_config", response_model=PlotConfig, tags=["candle data"])
|
||||
def plot_config(
|
||||
strategy: str | None = None,
|
||||
@@ -525,6 +524,29 @@ def list_available_pairs(
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/markets", response_model=MarketResponse, tags=["candle data", "webserver"])
|
||||
def markets(
|
||||
query: Annotated[MarketRequest, Query()],
|
||||
config=Depends(get_config),
|
||||
rpc: RPC | None = Depends(get_rpc_optional),
|
||||
):
|
||||
if not rpc or config["runmode"] == RunMode.WEBSERVER:
|
||||
# webserver mode
|
||||
config_loc = deepcopy(config)
|
||||
handleExchangePayload(query, config_loc)
|
||||
exchange = get_exchange(config_loc)
|
||||
else:
|
||||
exchange = rpc._freqtrade.exchange
|
||||
|
||||
return {
|
||||
"markets": exchange.get_markets(
|
||||
base_currencies=[query.base] if query.base else None,
|
||||
quote_currencies=[query.quote] if query.quote else None,
|
||||
),
|
||||
"exchange_id": exchange.id,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/sysinfo", response_model=SysInfo, tags=["info"])
|
||||
def sysinfo():
|
||||
return RPC._rpc_sysinfo()
|
||||
|
||||
@@ -120,6 +120,7 @@ class ApiServer(RPCHandler):
|
||||
from freqtrade.rpc.api_server.api_background_tasks import router as api_bg_tasks
|
||||
from freqtrade.rpc.api_server.api_backtest import router as api_backtest
|
||||
from freqtrade.rpc.api_server.api_download_data import router as api_download_data
|
||||
from freqtrade.rpc.api_server.api_pair_history import router as api_pair_history
|
||||
from freqtrade.rpc.api_server.api_pairlists import router as api_pairlists
|
||||
from freqtrade.rpc.api_server.api_v1 import router as api_v1
|
||||
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
|
||||
@@ -145,6 +146,11 @@ class ApiServer(RPCHandler):
|
||||
prefix="/api/v1",
|
||||
dependencies=[Depends(http_basic_or_jwt_token), Depends(is_webserver_mode)],
|
||||
)
|
||||
app.include_router(
|
||||
api_pair_history,
|
||||
prefix="/api/v1",
|
||||
dependencies=[Depends(http_basic_or_jwt_token), Depends(is_webserver_mode)],
|
||||
)
|
||||
app.include_router(
|
||||
api_pairlists,
|
||||
prefix="/api/v1",
|
||||
|
||||
+115
-49
@@ -6,13 +6,12 @@ import logging
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Generator, Sequence
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from math import isnan
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import psutil
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from dateutil.tz import tzlocal
|
||||
from numpy import inf, int64, mean, nan
|
||||
from numpy import inf, int64, isnan, mean, nan
|
||||
from pandas import DataFrame, NaT
|
||||
from sqlalchemy import func, select
|
||||
|
||||
@@ -31,10 +30,10 @@ from freqtrade.enums import (
|
||||
TradingMode,
|
||||
)
|
||||
from freqtrade.exceptions import ExchangeError, PricingError
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
||||
from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_msecs
|
||||
from freqtrade.exchange.exchange_utils import price_to_precision
|
||||
from freqtrade.loggers import bufferHandler
|
||||
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade
|
||||
from freqtrade.persistence import CustomDataWrapper, KeyStoreKeys, KeyValueStore, PairLocks, Trade
|
||||
from freqtrade.persistence.models import PairLock
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
@@ -42,12 +41,13 @@ from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||
from freqtrade.util import (
|
||||
decimals_per_coin,
|
||||
dt_from_ts,
|
||||
dt_humanize_delta,
|
||||
dt_now,
|
||||
dt_ts,
|
||||
dt_ts_def,
|
||||
format_date,
|
||||
shorten_date,
|
||||
)
|
||||
from freqtrade.util.datetime_helpers import dt_humanize_delta
|
||||
from freqtrade.wallets import PositionWallet, Wallet
|
||||
|
||||
|
||||
@@ -1115,31 +1115,70 @@ class RPC:
|
||||
"cancel_order_count": c_count,
|
||||
}
|
||||
|
||||
def _rpc_list_custom_data(self, trade_id: int, key: str | None) -> list[dict[str, Any]]:
|
||||
# Query for trade
|
||||
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
|
||||
if trade is None:
|
||||
return []
|
||||
# Query custom_data
|
||||
custom_data = []
|
||||
if key:
|
||||
data = trade.get_custom_data(key=key)
|
||||
if data:
|
||||
custom_data = [data]
|
||||
def _rpc_list_custom_data(
|
||||
self, trade_id: int | None = None, key: str | None = None, limit: int = 100, offset: int = 0
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Fetch custom data for a specific trade, or all open trades if `trade_id` is not provided.
|
||||
Pagination is applied via `limit` and `offset`.
|
||||
|
||||
Returns an array of dictionaries, each containing:
|
||||
- "trade_id": the ID of the trade (int)
|
||||
- "custom_data": a list of custom data dicts, each with the fields:
|
||||
"id", "key", "type", "value", "created_at", "updated_at"
|
||||
"""
|
||||
trades: Sequence[Trade]
|
||||
if trade_id is None:
|
||||
# Get all open trades
|
||||
trades = Trade.session.scalars(
|
||||
Trade.get_trades_query([Trade.is_open.is_(True)])
|
||||
.order_by(Trade.id)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
).all()
|
||||
else:
|
||||
custom_data = trade.get_all_custom_data()
|
||||
return [
|
||||
{
|
||||
"id": data_entry.id,
|
||||
"ft_trade_id": data_entry.ft_trade_id,
|
||||
"cd_key": data_entry.cd_key,
|
||||
"cd_type": data_entry.cd_type,
|
||||
"cd_value": data_entry.cd_value,
|
||||
"created_at": data_entry.created_at,
|
||||
"updated_at": data_entry.updated_at,
|
||||
}
|
||||
for data_entry in custom_data
|
||||
]
|
||||
trades = Trade.get_trades(trade_filter=[Trade.id == trade_id]).all()
|
||||
|
||||
if not trades:
|
||||
raise RPCException(
|
||||
f"No trade found for trade_id: {trade_id}" if trade_id else "No open trades found."
|
||||
)
|
||||
|
||||
results = []
|
||||
for trade in trades:
|
||||
# Depending on whether a specific key is provided, retrieve custom data accordingly.
|
||||
if key:
|
||||
data = trade.get_custom_data_entry(key=key)
|
||||
# If data exists, wrap it in a list so the output remains consistent.
|
||||
custom_data = [data] if data else []
|
||||
else:
|
||||
custom_data = trade.get_all_custom_data()
|
||||
|
||||
# Format and Append result for the trade if any custom data was found.
|
||||
if custom_data:
|
||||
formatted_custom_data = [
|
||||
{
|
||||
"key": data_entry.cd_key,
|
||||
"type": data_entry.cd_type,
|
||||
"value": CustomDataWrapper._convert_custom_data(data_entry).value,
|
||||
"created_at": data_entry.created_at,
|
||||
"updated_at": data_entry.updated_at,
|
||||
}
|
||||
for data_entry in custom_data
|
||||
]
|
||||
results.append({"trade_id": trade.id, "custom_data": formatted_custom_data})
|
||||
|
||||
# Handle case when there is no custom data found across trades.
|
||||
if not results:
|
||||
message_details = ""
|
||||
if key:
|
||||
message_details += f"with key '{key}' "
|
||||
message_details += (
|
||||
f"found for Trade ID: {trade_id}." if trade_id else "found for any open trades."
|
||||
)
|
||||
raise RPCException(f"No custom-data {message_details}")
|
||||
|
||||
return results
|
||||
|
||||
def _rpc_performance(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
@@ -1436,7 +1475,12 @@ class RPC:
|
||||
|
||||
@staticmethod
|
||||
def _rpc_analysed_history_full(
|
||||
config: Config, pair: str, timeframe: str, exchange, selected_cols: list[str] | None
|
||||
config: Config,
|
||||
pair: str,
|
||||
timeframe: str,
|
||||
exchange: Exchange,
|
||||
selected_cols: list[str] | None,
|
||||
live: bool,
|
||||
) -> dict[str, Any]:
|
||||
timerange_parsed = TimeRange.parse_timerange(config.get("timerange"))
|
||||
|
||||
@@ -1444,31 +1488,53 @@ class RPC:
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
|
||||
strategy = StrategyResolver.load_strategy(config)
|
||||
startup_candles = strategy.startup_candle_count
|
||||
strategy_name = ""
|
||||
startup_candles = 0
|
||||
if config.get("strategy"):
|
||||
strategy = StrategyResolver.load_strategy(config)
|
||||
startup_candles = strategy.startup_candle_count
|
||||
strategy_name = strategy.get_strategy_name()
|
||||
|
||||
_data = load_data(
|
||||
datadir=config["datadir"],
|
||||
pairs=[pair],
|
||||
timeframe=timeframe,
|
||||
timerange=timerange_parsed,
|
||||
data_format=config["dataformat_ohlcv"],
|
||||
candle_type=config.get("candle_type_def", CandleType.SPOT),
|
||||
startup_candles=startup_candles,
|
||||
)
|
||||
if pair not in _data:
|
||||
raise RPCException(
|
||||
f"No data for {pair}, {timeframe} in {config.get('timerange')} found."
|
||||
if live:
|
||||
data = exchange.get_historic_ohlcv(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=timerange_parsed.startts * 1000
|
||||
if timerange_parsed.startts
|
||||
else dt_ts(dt_now() - timedelta(days=30)),
|
||||
is_new_pair=True, # history is never available - so always treat as new pair
|
||||
candle_type=config.get("candle_type_def", CandleType.SPOT),
|
||||
until_ms=timerange_parsed.stopts,
|
||||
)
|
||||
else:
|
||||
_data = load_data(
|
||||
datadir=config["datadir"],
|
||||
pairs=[pair],
|
||||
timeframe=timeframe,
|
||||
timerange=timerange_parsed,
|
||||
data_format=config["dataformat_ohlcv"],
|
||||
candle_type=config.get("candle_type_def", CandleType.SPOT),
|
||||
startup_candles=startup_candles,
|
||||
)
|
||||
if pair not in _data:
|
||||
raise RPCException(
|
||||
f"No data for {pair}, {timeframe} in {config.get('timerange')} found."
|
||||
)
|
||||
data = _data[pair]
|
||||
|
||||
strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
|
||||
strategy.ft_bot_start()
|
||||
if config.get("strategy"):
|
||||
strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
|
||||
strategy.ft_bot_start()
|
||||
|
||||
df_analyzed = strategy.analyze_ticker(_data[pair], {"pair": pair})
|
||||
df_analyzed = trim_dataframe(df_analyzed, timerange_parsed, startup_candles=startup_candles)
|
||||
df_analyzed = strategy.analyze_ticker(data, {"pair": pair})
|
||||
df_analyzed = trim_dataframe(
|
||||
df_analyzed, timerange_parsed, startup_candles=startup_candles
|
||||
)
|
||||
else:
|
||||
df_analyzed = data
|
||||
|
||||
return RPC._convert_dataframe_to_dict(
|
||||
strategy.get_strategy_name(),
|
||||
strategy_name,
|
||||
pair,
|
||||
timeframe,
|
||||
df_analyzed.copy(),
|
||||
|
||||
+27
-17
@@ -25,6 +25,7 @@ from telegram import (
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
KeyboardButton,
|
||||
Message,
|
||||
ReplyKeyboardMarkup,
|
||||
Update,
|
||||
)
|
||||
@@ -96,17 +97,17 @@ def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]):
|
||||
"""
|
||||
|
||||
@wraps(command_handler)
|
||||
async def wrapper(self, *args, **kwargs):
|
||||
async def wrapper(self, *args, **kwargs) -> None:
|
||||
"""Decorator logic"""
|
||||
update = kwargs.get("update") or args[0]
|
||||
|
||||
# Reject unauthorized messages
|
||||
if update.callback_query:
|
||||
cchat_id = int(update.callback_query.message.chat.id)
|
||||
ctopic_id = update.callback_query.message.message_thread_id
|
||||
else:
|
||||
cchat_id = int(update.message.chat_id)
|
||||
ctopic_id = update.message.message_thread_id
|
||||
message: Message = (
|
||||
update.message if update.callback_query is None else update.callback_query.message
|
||||
)
|
||||
cchat_id: int = int(message.chat_id)
|
||||
ctopic_id: int | None = message.message_thread_id
|
||||
from_user_id: str = str(update.effective_user.id if update.effective_user else "")
|
||||
|
||||
chat_id = int(self._config["telegram"]["chat_id"])
|
||||
if cchat_id != chat_id:
|
||||
@@ -118,6 +119,10 @@ def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]):
|
||||
logger.debug(f"Rejected message from wrong channel: {cchat_id}, {ctopic_id}")
|
||||
return None
|
||||
|
||||
authorized = self._config["telegram"].get("authorized_users", None)
|
||||
if authorized is not None and from_user_id not in authorized:
|
||||
logger.info(f"Unauthorized user tried to control the bot: {from_user_id}")
|
||||
return None
|
||||
# Rollback session to avoid getting data stored in a transaction.
|
||||
Trade.rollback()
|
||||
logger.debug("Executing handler: %s for chat_id: %s", command_handler.__name__, chat_id)
|
||||
@@ -1976,16 +1981,17 @@ class Telegram(RPCHandler):
|
||||
results = self._rpc._rpc_list_custom_data(trade_id, key)
|
||||
messages = []
|
||||
if len(results) > 0:
|
||||
messages.append("Found custom-data entr" + ("ies: " if len(results) > 1 else "y: "))
|
||||
for result in results:
|
||||
trade_custom_data = results[0]["custom_data"]
|
||||
messages.append(
|
||||
"Found custom-data entr" + ("ies: " if len(trade_custom_data) > 1 else "y: ")
|
||||
)
|
||||
for custom_data in trade_custom_data:
|
||||
lines = [
|
||||
f"*Key:* `{result['cd_key']}`",
|
||||
f"*ID:* `{result['id']}`",
|
||||
f"*Trade ID:* `{result['ft_trade_id']}`",
|
||||
f"*Type:* `{result['cd_type']}`",
|
||||
f"*Value:* `{result['cd_value']}`",
|
||||
f"*Create Date:* `{format_date(result['created_at'])}`",
|
||||
f"*Update Date:* `{format_date(result['updated_at'])}`",
|
||||
f"*Key:* `{custom_data['key']}`",
|
||||
f"*Type:* `{custom_data['type']}`",
|
||||
f"*Value:* `{custom_data['value']}`",
|
||||
f"*Create Date:* `{format_date(custom_data['created_at'])}`",
|
||||
f"*Update Date:* `{format_date(custom_data['updated_at'])}`",
|
||||
]
|
||||
# Filter empty lines using list-comprehension
|
||||
messages.append("\n".join([line for line in lines if line]))
|
||||
@@ -2153,6 +2159,9 @@ class Telegram(RPCHandler):
|
||||
return
|
||||
chat_id = update.message.chat_id
|
||||
topic_id = update.message.message_thread_id
|
||||
user_id = (
|
||||
update.effective_user.id if topic_id is not None and update.effective_user else None
|
||||
)
|
||||
|
||||
msg = f"""Freqtrade Bot Info:
|
||||
```json
|
||||
@@ -2160,7 +2169,8 @@ class Telegram(RPCHandler):
|
||||
"enabled": true,
|
||||
"token": "********",
|
||||
"chat_id": "{chat_id}",
|
||||
{f'"topic_id": "{topic_id}"' if topic_id else ""}
|
||||
{f'"topic_id": "{topic_id}",' if topic_id else ""}
|
||||
{f'//"authorized_users": ["{user_id}"]' if topic_id and user_id else ""}
|
||||
}}
|
||||
```
|
||||
"""
|
||||
|
||||
@@ -132,6 +132,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
stake_currency: str
|
||||
# container variable for strategy source code
|
||||
__source__: str = ""
|
||||
__file__: str = ""
|
||||
|
||||
# Definition of plot_config. See plotting documentation for more details.
|
||||
plot_config: dict = {}
|
||||
@@ -664,7 +665,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
entry_tag: str | None,
|
||||
side: str,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
) -> float | None:
|
||||
"""
|
||||
Entry price re-adjustment logic, returning the user desired limit price.
|
||||
This only executes when a order was already placed, still open (unfilled fully or partially)
|
||||
@@ -685,11 +686,109 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New entry price value if provided
|
||||
:return float or None: New entry price value if provided
|
||||
|
||||
"""
|
||||
return current_order_rate
|
||||
|
||||
def adjust_exit_price(
|
||||
self,
|
||||
trade: Trade,
|
||||
order: Order | None,
|
||||
pair: str,
|
||||
current_time: datetime,
|
||||
proposed_rate: float,
|
||||
current_order_rate: float,
|
||||
entry_tag: str | None,
|
||||
side: str,
|
||||
**kwargs,
|
||||
) -> float | None:
|
||||
"""
|
||||
Exit price re-adjustment logic, returning the user desired limit price.
|
||||
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/
|
||||
|
||||
When not implemented by a strategy, returns current_order_rate as default.
|
||||
If current_order_rate is returned then the existing order is maintained.
|
||||
If None is returned then order gets canceled but not replaced by a new one.
|
||||
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param trade: Trade object.
|
||||
:param order: Order object
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
|
||||
:param current_order_rate: Rate of the existing order in place.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float or None: New exit price value if provided
|
||||
|
||||
"""
|
||||
return current_order_rate
|
||||
|
||||
def adjust_order_price(
|
||||
self,
|
||||
trade: Trade,
|
||||
order: Order | None,
|
||||
pair: str,
|
||||
current_time: datetime,
|
||||
proposed_rate: float,
|
||||
current_order_rate: float,
|
||||
entry_tag: str | None,
|
||||
side: str,
|
||||
is_entry: bool,
|
||||
**kwargs,
|
||||
) -> float | None:
|
||||
"""
|
||||
Exit and entry order price re-adjustment logic, returning the user desired limit price.
|
||||
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/
|
||||
|
||||
When not implemented by a strategy, returns current_order_rate as default.
|
||||
If current_order_rate is returned then the existing order is maintained.
|
||||
If None is returned then order gets canceled but not replaced by a new one.
|
||||
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param trade: Trade object.
|
||||
:param order: Order object
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
|
||||
:param current_order_rate: Rate of the existing order in place.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param is_entry: True if the order is an entry order, False if it's an exit order.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float or None: New entry price value if provided
|
||||
"""
|
||||
if is_entry:
|
||||
return self.adjust_entry_price(
|
||||
trade=trade,
|
||||
order=order,
|
||||
pair=pair,
|
||||
current_time=current_time,
|
||||
proposed_rate=proposed_rate,
|
||||
current_order_rate=current_order_rate,
|
||||
entry_tag=entry_tag,
|
||||
side=side,
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
return self.adjust_exit_price(
|
||||
trade=trade,
|
||||
order=order,
|
||||
pair=pair,
|
||||
current_time=current_time,
|
||||
proposed_rate=proposed_rate,
|
||||
current_order_rate=current_order_rate,
|
||||
entry_tag=entry_tag,
|
||||
side=side,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def leverage(
|
||||
self,
|
||||
pair: str,
|
||||
|
||||
@@ -40,7 +40,7 @@ def custom_entry_price(
|
||||
"""
|
||||
return proposed_rate
|
||||
|
||||
def adjust_entry_price(
|
||||
def adjust_order_price(
|
||||
self,
|
||||
trade: Trade,
|
||||
order: Order | None,
|
||||
@@ -50,10 +50,11 @@ def adjust_entry_price(
|
||||
current_order_rate: float,
|
||||
entry_tag: str | None,
|
||||
side: str,
|
||||
is_entry: bool,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
) -> float | None:
|
||||
"""
|
||||
Entry price re-adjustment logic, returning the user desired limit price.
|
||||
Exit and entry order price re-adjustment logic, returning the user desired limit price.
|
||||
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.
|
||||
|
||||
@@ -71,9 +72,9 @@ def adjust_entry_price(
|
||||
:param current_order_rate: Rate of the existing order in place.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param is_entry: True if the order is an entry order, False if it's an exit order.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New entry price value if provided
|
||||
|
||||
:return float or None: New entry price value if provided
|
||||
"""
|
||||
return current_order_rate
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from rich.progress import (
|
||||
TimeRemainingColumn,
|
||||
)
|
||||
|
||||
from freqtrade.loggers import error_console
|
||||
from freqtrade.util.rich_progress import CustomProgress
|
||||
|
||||
|
||||
@@ -21,6 +20,8 @@ def get_progress_tracker(**kwargs) -> CustomProgress:
|
||||
"""
|
||||
Get progress Bar with custom columns.
|
||||
"""
|
||||
from freqtrade.loggers import error_console
|
||||
|
||||
return CustomProgress(
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(bar_width=None),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, TypeAlias
|
||||
|
||||
from pandas import DataFrame
|
||||
from rich.console import Console
|
||||
from rich.table import Column, Table
|
||||
from rich.text import Text
|
||||
|
||||
from freqtrade.loggers.rich_console import get_rich_console
|
||||
|
||||
|
||||
TextOrString: TypeAlias = str | Text
|
||||
|
||||
@@ -38,11 +38,7 @@ def print_rich_table(
|
||||
row_to_add: list[str | Text] = [r if isinstance(r, Text) else str(r) for r in row]
|
||||
table.add_row(*row_to_add)
|
||||
|
||||
width = None
|
||||
if any(module in ["pytest", "ipykernel"] for module in sys.modules):
|
||||
width = 200
|
||||
|
||||
console = Console(width=width)
|
||||
console = get_rich_console()
|
||||
console.print(table)
|
||||
|
||||
|
||||
@@ -74,9 +70,5 @@ def print_df_rich_table(
|
||||
row = [_format_value(x, floatfmt=".3f") for x in value_list]
|
||||
table.add_row(*row)
|
||||
|
||||
width = None
|
||||
if any(module in ["pytest", "ipykernel"] for module in sys.modules):
|
||||
width = 200
|
||||
|
||||
console = Console(width=width)
|
||||
console = get_rich_console()
|
||||
console.print(table)
|
||||
|
||||
@@ -197,7 +197,7 @@ class Wallets:
|
||||
# Position is not open ...
|
||||
continue
|
||||
size = self._exchange._contracts_to_amount(symbol, position["contracts"])
|
||||
collateral = safe_value_fallback(position, "collateral", "initialMargin", 0.0)
|
||||
collateral = safe_value_fallback(position, "initialMargin", "collateral", 0.0)
|
||||
leverage = position.get("leverage")
|
||||
_parsed_positions[symbol] = PositionWallet(
|
||||
symbol,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from freqtrade_client.ft_rest_client import FtRestClient
|
||||
|
||||
|
||||
__version__ = "2025.1"
|
||||
__version__ = "2025.3"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -23,10 +23,18 @@ PostDataT = dict[str, Any] | list[dict[str, Any]] | None
|
||||
|
||||
class FtRestClient:
|
||||
def __init__(
|
||||
self, serverurl, username=None, password=None, *, pool_connections=10, pool_maxsize=10
|
||||
self,
|
||||
serverurl,
|
||||
username=None,
|
||||
password=None,
|
||||
*,
|
||||
pool_connections=10,
|
||||
pool_maxsize=10,
|
||||
timeout=10,
|
||||
):
|
||||
self._serverurl = serverurl
|
||||
self._session = requests.Session()
|
||||
self._timeout = timeout
|
||||
|
||||
# allow configuration of pool
|
||||
adapter = HTTPAdapter(pool_connections=pool_connections, pool_maxsize=pool_maxsize)
|
||||
@@ -50,7 +58,9 @@ class FtRestClient:
|
||||
url = urlunparse((schema, netloc, path, par, query, fragment))
|
||||
|
||||
try:
|
||||
resp = self._session.request(method, url, headers=hd, data=json.dumps(data))
|
||||
resp = self._session.request(
|
||||
method, url, headers=hd, timeout=self._timeout, data=json.dumps(data)
|
||||
)
|
||||
# return resp.text
|
||||
return resp.json()
|
||||
except RequestConnectionError:
|
||||
@@ -259,6 +269,36 @@ class FtRestClient:
|
||||
params["offset"] = offset
|
||||
return self._get("trades", params)
|
||||
|
||||
def list_open_trades_custom_data(self, key=None, limit=100, offset=0):
|
||||
"""List open trades custom-data of the running bot.
|
||||
|
||||
:param key: str, optional - Key of the custom-data
|
||||
:param limit: limit of trades
|
||||
:param offset: trades offset for pagination
|
||||
:return: json object
|
||||
"""
|
||||
params = {}
|
||||
params["limit"] = limit
|
||||
params["offset"] = offset
|
||||
if key is not None:
|
||||
params["key"] = key
|
||||
|
||||
return self._get("trades/open/custom-data", params=params)
|
||||
|
||||
def list_custom_data(self, trade_id, key=None):
|
||||
"""List custom-data of the running bot for a specific trade.
|
||||
|
||||
:param trade_id: ID of the trade
|
||||
:param key: str, optional - Key of the custom-data
|
||||
:return: JSON object
|
||||
"""
|
||||
params = {}
|
||||
params["trade_id"] = trade_id
|
||||
if key is not None:
|
||||
params["key"] = key
|
||||
|
||||
return self._get(f"trades/{trade_id}/custom-data", params=params)
|
||||
|
||||
def trade(self, trade_id):
|
||||
"""Return specific trade
|
||||
|
||||
|
||||
+1
-3
@@ -29,7 +29,7 @@ classifiers = [
|
||||
|
||||
dependencies = [
|
||||
# from requirements.txt
|
||||
"ccxt>=4.3.24",
|
||||
"ccxt>=4.4.60",
|
||||
"SQLAlchemy>=2.0.6",
|
||||
"python-telegram-bot>=20.1",
|
||||
"humanize>=4.0.0",
|
||||
@@ -287,8 +287,6 @@ max-complexity = 12
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"freqtrade/freqai/**/*.py" = [
|
||||
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
"B006", # Bugbear - mutable default argument
|
||||
"B008", # bugbear - Do not perform function calls in argument defaults
|
||||
]
|
||||
"tests/**/*.py" = [
|
||||
"S101", # allow assert in tests
|
||||
|
||||
@@ -7,26 +7,26 @@
|
||||
-r docs/requirements-docs.txt
|
||||
|
||||
coveralls==4.0.1
|
||||
ruff==0.9.3
|
||||
mypy==1.14.1
|
||||
pre-commit==4.1.0
|
||||
pytest==8.3.4
|
||||
pytest-asyncio==0.25.2
|
||||
ruff==0.11.2
|
||||
mypy==1.15.0
|
||||
pre-commit==4.2.0
|
||||
pytest==8.3.5
|
||||
pytest-asyncio==0.25.3
|
||||
pytest-cov==6.0.0
|
||||
pytest-mock==3.14.0
|
||||
pytest-random-order==1.1.1
|
||||
pytest-timeout==2.3.1
|
||||
pytest-xdist==3.6.1
|
||||
isort==5.13.2
|
||||
isort==6.0.1
|
||||
# For datetime mocking
|
||||
time-machine==2.16.0
|
||||
|
||||
# Convert jupyter notebooks to markdown documents
|
||||
nbconvert==7.16.5
|
||||
nbconvert==7.16.6
|
||||
|
||||
# mypy types
|
||||
types-cachetools==5.5.0.20240820
|
||||
types-filelock==3.2.7
|
||||
types-requests==2.32.0.20241016
|
||||
types-requests==2.32.0.20250306
|
||||
types-tabulate==0.9.0.20241207
|
||||
types-python-dateutil==2.9.0.20241206
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
|
||||
# Required for freqai-rl
|
||||
torch==2.2.2; sys_platform == 'darwin' and platform_machine == 'x86_64'
|
||||
torch==2.5.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
||||
torch==2.6.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
||||
gymnasium==0.29.1
|
||||
stable_baselines3==2.4.1
|
||||
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
|
||||
stable_baselines3==2.4.1; sys_platform == 'darwin' and platform_machine == 'x86_64'
|
||||
stable_baselines3==2.5.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
||||
sb3_contrib>=2.2.1
|
||||
# Progress bar for stable-baselines3 and sb3-contrib
|
||||
tqdm==4.67.1
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
scikit-learn==1.6.1
|
||||
joblib==1.4.2
|
||||
catboost==1.2.7; 'arm' not in platform_machine
|
||||
lightgbm==4.5.0
|
||||
xgboost==2.1.3
|
||||
tensorboard==2.18.0
|
||||
lightgbm==4.6.0
|
||||
xgboost==2.1.4
|
||||
tensorboard==2.19.0
|
||||
datasieve==0.1.7
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-r requirements.txt
|
||||
|
||||
# Required for hyperopt
|
||||
scipy==1.15.1
|
||||
scipy==1.15.2
|
||||
scikit-learn==1.6.1
|
||||
ft-scikit-optimize==0.9.2
|
||||
filelock==3.17.0
|
||||
filelock==3.18.0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user