Compare commits
515 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c079ccdfd | |||
| 5407c2efec | |||
| cee8855dca | |||
| 1196237f82 | |||
| 71e1c91148 | |||
| e9718e064f | |||
| 3e8bb29c9f | |||
| 2fd703fcc5 | |||
| 2a84b00dad | |||
| 711ebfea28 | |||
| 8cd2ee95f9 | |||
| 539937c0eb | |||
| fefd698e96 | |||
| 3512d788dc | |||
| 54c4dd1ce6 | |||
| daaff66ff0 | |||
| 8610f9883d | |||
| e503d4a9c0 | |||
| 1cf1d9e3d7 | |||
| 6b250f9dfe | |||
| 3d24ef66d8 | |||
| ef0fc13235 | |||
| 394ff1bbf6 | |||
| 5cdd087465 | |||
| 93051f4f97 | |||
| 1c2eed1421 | |||
| 532f5ab080 | |||
| e3ffe56549 | |||
| 25d4cabe3b | |||
| 7539b6ffda | |||
| 8e623ee64c | |||
| 22ddd9a7d4 | |||
| 128ea45fee | |||
| 2495f68751 | |||
| 70ee652be5 | |||
| bb1d34c33d | |||
| 8f6d64f5e1 | |||
| b40848373d | |||
| 2824bcff41 | |||
| e8b967d634 | |||
| 12d617cf8d | |||
| 48ab5ed990 | |||
| 3363b1062e | |||
| d35dde6757 | |||
| 6016d4886e | |||
| f77eaa4083 | |||
| 460d88ced7 | |||
| 6bb559f1b0 | |||
| 7c91eb0091 | |||
| ae672e1110 | |||
| 1c4bf67e29 | |||
| c4e671fdc3 | |||
| c5c5198fb3 | |||
| 634ec3c85f | |||
| d15aa4b337 | |||
| e160739a28 | |||
| 030e07030c | |||
| a50a15ebc6 | |||
| 514452c4ec | |||
| f091db3b0e | |||
| caf1d6e7f2 | |||
| 81f6637803 | |||
| f764f1fc10 | |||
| 9a8ef17c73 | |||
| 8fe582a7a7 | |||
| 5cf3a2256a | |||
| caf4ffbc0b | |||
| d156f36f99 | |||
| 4390324d22 | |||
| 8d7ef1929e | |||
| 08fb8e42f7 | |||
| eaf6abecc8 | |||
| b4796cbf73 | |||
| e042b2e07b | |||
| 9f021cbee6 | |||
| bf615e415b | |||
| c72f5b79fe | |||
| 51c1674663 | |||
| e445452d35 | |||
| 9fff38d0a3 | |||
| bdd69dbfbc | |||
| ec4767acd5 | |||
| 78c8054954 | |||
| 24f5096ec6 | |||
| 030bc8641b | |||
| c9403eb946 | |||
| 87b0a6d3f3 | |||
| c0d404edb5 | |||
| a7a16d1e1e | |||
| 1f21d5e2c2 | |||
| a531f8644d | |||
| 765a0b52cb | |||
| 4919d6944e | |||
| ac20596f10 | |||
| 5e79a93235 | |||
| 2e8decbb6d | |||
| 196e599dc8 | |||
| 485cad2dc6 | |||
| 61cbe0c4b1 | |||
| 5510e138db | |||
| c9e9f9bd82 | |||
| 768ba1baaf | |||
| ad25a60149 | |||
| 3fdcd6e8f1 | |||
| be0c0761e3 | |||
| 00dd3f05ac | |||
| c6f30989c9 | |||
| 4ab6fba9e7 | |||
| 3ef082e2d9 | |||
| cccf44cd68 | |||
| 506d665a43 | |||
| 8514e35ad0 | |||
| af0a3f9a8d | |||
| 2df1a93dfc | |||
| 75e39d3154 | |||
| 5d755e3ca0 | |||
| fbeb2e4789 | |||
| 6b288d46af | |||
| 394cdfcc15 | |||
| 28df02da82 | |||
| d4ee725747 | |||
| 6617d3bb46 | |||
| 2453e344f4 | |||
| 7f0e886cfd | |||
| e34ea393d9 | |||
| a00a5e52c4 | |||
| 6abc0a3fb9 | |||
| 7e4b9d7481 | |||
| e3f241acb7 | |||
| eee233f894 | |||
| 615095c780 | |||
| b22bce3dd8 | |||
| 9f1d9add18 | |||
| 36f2bef6b8 | |||
| 07906d2576 | |||
| 5c977c212b | |||
| 09b084e85a | |||
| aefc72554a | |||
| 33c2cc806a | |||
| 74aa17d534 | |||
| 4de1501c68 | |||
| dc12b3e750 | |||
| dacd09db8b | |||
| c63d0e167c | |||
| 6ccd98d795 | |||
| 52b0e64950 | |||
| 5e5137edc1 | |||
| 5d4edb5ec0 | |||
| 6dccab9bfe | |||
| 653c160d76 | |||
| 3f65ed452e | |||
| 09cdc7ceca | |||
| e98e6d8aee | |||
| f10223bf13 | |||
| 38b3062653 | |||
| 6063e4accb | |||
| 06e249f569 | |||
| d064bd4f34 | |||
| 765d2c332c | |||
| 8857c77645 | |||
| 1ff4e40824 | |||
| 09d59c79a7 | |||
| dd6ac4b196 | |||
| 67fe3682b5 | |||
| a07e6183bb | |||
| ac99537c25 | |||
| 734a6f0442 | |||
| da52ef8729 | |||
| c934535dc8 | |||
| 3e3f092f0c | |||
| 834aafb8f2 | |||
| df19c77bcf | |||
| 6edcda1e42 | |||
| fe8c367bce | |||
| a37edf8c62 | |||
| 09ef2c9dea | |||
| 42b1d8ecf8 | |||
| 32437a95f3 | |||
| 2e29d3523b | |||
| 530300a951 | |||
| 612a09634c | |||
| fd6a2456d0 | |||
| 1ad0e7e365 | |||
| 055ee5c88f | |||
| ae676d8e03 | |||
| 0dc665e45b | |||
| bc6ea16db8 | |||
| a1d0528adb | |||
| ecfa1fd5b5 | |||
| eaf76047e3 | |||
| 761d3f216e | |||
| e7d9ac2247 | |||
| 842e651928 | |||
| 091d1c2d55 | |||
| 47bf2d1f26 | |||
| 758392c206 | |||
| 647011bf0c | |||
| 580413b5f8 | |||
| c8c9e5167e | |||
| 2e8707059d | |||
| e0d9d01493 | |||
| 45bf1ace7d | |||
| 39a5328f5a | |||
| 9f82738c96 | |||
| f69f6ec8be | |||
| 8b3fb11f14 | |||
| b0f4c57fa8 | |||
| d759deae8f | |||
| b695b5a83c | |||
| b8883b790e | |||
| 423197cb23 | |||
| 38107664b5 | |||
| 42d3f557bc | |||
| 1a3c913176 | |||
| 83de41ad0e | |||
| 8cc4664823 | |||
| aaecbc2206 | |||
| e36df7eff5 | |||
| 95f58b2b8c | |||
| 8df0ce3fb5 | |||
| 4177809f71 | |||
| 96aa214fc5 | |||
| a3c1c8c1b4 | |||
| 2bf3494cbc | |||
| e3dd3478bb | |||
| 457af15b35 | |||
| 1eef1675cc | |||
| 89ef31b381 | |||
| c00472c42d | |||
| c0143b04c8 | |||
| d1c901ae94 | |||
| dd0aeaf380 | |||
| 3e8e250a65 | |||
| a34b580444 | |||
| e7ea9d09c9 | |||
| e4e11be331 | |||
| 9774c51a97 | |||
| 87ff8dc7bc | |||
| 2434ae5476 | |||
| e474294d55 | |||
| b081716df7 | |||
| e1af43b133 | |||
| 1ea6a99c48 | |||
| 79fc80957f | |||
| 937eedd8ca | |||
| 93ae45dcbb | |||
| 1adbd525bc | |||
| ced7465c1f | |||
| d635cb8516 | |||
| 2b578633ab | |||
| 824f555d48 | |||
| 999fd15bee | |||
| be07879a1e | |||
| ab543c2680 | |||
| 79aa0bce5a | |||
| 05a76c0297 | |||
| 15eeac3c48 | |||
| d8a84a7d6b | |||
| 863a5b7b81 | |||
| 4ad049aec3 | |||
| 03c16d034d | |||
| c1f8dce109 | |||
| 887f513438 | |||
| 12fbae7204 | |||
| a05cb8f1c5 | |||
| cffee80b0e | |||
| 0c0f2e43e8 | |||
| 35456a1f32 | |||
| b6a6bca41b | |||
| 102cc87618 | |||
| 1ec45dcf19 | |||
| f2fb1b7919 | |||
| 78c5b18937 | |||
| 86de1dee29 | |||
| 7afd3cb730 | |||
| b887e2ad21 | |||
| ea2350f1ed | |||
| 786c858452 | |||
| a6b643a33f | |||
| 0e5235e6dc | |||
| c93c97c2de | |||
| 271ba462b1 | |||
| 690918f8d3 | |||
| de5dd66512 | |||
| c8593b2307 | |||
| fc21757e40 | |||
| b6d0cde29f | |||
| f0dfc23a5f | |||
| c9dda3480d | |||
| 45804e1bef | |||
| 6dd5dc5d2c | |||
| a5db75d361 | |||
| 4c4bc2be9b | |||
| e9cc9bb38a | |||
| 3b6395ddb8 | |||
| fdcbb3c0d0 | |||
| b07ffbf721 | |||
| a7b7e10c09 | |||
| f18472479a | |||
| 18c37b9b8f | |||
| d28abc18ea | |||
| 80b50141ee | |||
| b15d49f9ae | |||
| 668635892c | |||
| 670a29d420 | |||
| fd0d089f43 | |||
| b5fb6888f0 | |||
| edc23dcd2f | |||
| 1dc299f6f4 | |||
| 99d2423534 | |||
| b8b8da2eb2 | |||
| 4237d9fd11 | |||
| b98a91ba4c | |||
| e74db3e024 | |||
| ead3282229 | |||
| dfd9e7e777 | |||
| 71362c9140 | |||
| 68114691c6 | |||
| 37a3bdd507 | |||
| 50389cbbf4 | |||
| a7e5d3e427 | |||
| 48d1106546 | |||
| 6452521002 | |||
| 52374b39f2 | |||
| 300a866aac | |||
| 9873a862e0 | |||
| 1c8bbc14b8 | |||
| afa1311ff8 | |||
| 3ecc9c30cb | |||
| a85bb040f2 | |||
| 841e480ff5 | |||
| a0e9b77f75 | |||
| 47d59f8ff4 | |||
| 676c48411b | |||
| 954950c8cb | |||
| 2520b7cd51 | |||
| f9d7f05d38 | |||
| 46d4ff3d36 | |||
| 9347d823fc | |||
| 5e8bd359c8 | |||
| bae8a55f81 | |||
| 56c672b6d2 | |||
| de3956f2f9 | |||
| ddd7009fa8 | |||
| 31891b25af | |||
| 187b19b1d6 | |||
| e55d8512e7 | |||
| 697912f244 | |||
| 2f3ad09cf5 | |||
| 523dd305ba | |||
| 2d87d74829 | |||
| 820791e889 | |||
| a130122cc6 | |||
| 3a58297a8d | |||
| 65d22f9f1c | |||
| ec5df87318 | |||
| cf1533c478 | |||
| fc34f8cad2 | |||
| 3fbb38730c | |||
| 6a327440e0 | |||
| f1c8d8325b | |||
| d2557a70d7 | |||
| bf76347751 | |||
| a99a49e2af | |||
| b7fc924e82 | |||
| ef55bf6800 | |||
| 25ffc76655 | |||
| 15b1cff9c2 | |||
| c1ba2936ec | |||
| f08c0c9870 | |||
| 3b5b8bd007 | |||
| 74067d454b | |||
| 16359d617d | |||
| ae9f83ed30 | |||
| 49626ca31c | |||
| 884f543fb7 | |||
| 29df7499c5 | |||
| 5e64416dfa | |||
| 1118032c1f | |||
| 277828b1d4 | |||
| 47dcf7bc93 | |||
| 75081ab046 | |||
| 571497e58b | |||
| d710c85cda | |||
| 978f9c804b | |||
| 31522c681e | |||
| a5ac8a95a7 | |||
| 35dab9b566 | |||
| 2b05a49671 | |||
| b79b5b6c32 | |||
| 78124cd025 | |||
| 7c4c789711 | |||
| 30b32a0d2e | |||
| d33768ecee | |||
| ccf20b2fe1 | |||
| 23bbbddcdb | |||
| c6ed64746c | |||
| 8f4dfbf55d | |||
| 5296a3be23 | |||
| c92c64bac2 | |||
| aaca904455 | |||
| 19b57ad87e | |||
| 583738040c | |||
| 6e38b72601 | |||
| 8bf8ccfe50 | |||
| e721a741ca | |||
| 19e9effae4 | |||
| 84100ca7ae | |||
| a6f94f7d24 | |||
| 0a32be3fc3 | |||
| 065203c3db | |||
| 758ae42092 | |||
| 6fdc0f1b22 | |||
| 7c9fe22b9f | |||
| 53aecfa856 | |||
| bf511f12d4 | |||
| 0eed655e7c | |||
| d2d283a4b2 | |||
| 85492c1084 | |||
| f07134bf66 | |||
| 7678a59b1f | |||
| daced63d00 | |||
| 002d8c4022 | |||
| 8c85448ed7 | |||
| 2236b08fc2 | |||
| 24f904efc4 | |||
| 3eaa862caf | |||
| 469587e656 | |||
| b24064d706 | |||
| c4a29a0178 | |||
| aedbe0d1d2 | |||
| c5e3f7d0ba | |||
| bc019d6b6d | |||
| f889061b95 | |||
| 1f52ff3f94 | |||
| d0c3b30289 | |||
| e77feafee0 | |||
| 1889a315a3 | |||
| cc998afb44 | |||
| 97f30cf13d | |||
| ccbc48b590 | |||
| dde6001ad0 | |||
| aae3ab23ac | |||
| 604e08382e | |||
| 2ecadabd92 | |||
| 17c7826756 | |||
| fe92df7842 | |||
| e6dd932436 | |||
| 8acc05bb2d | |||
| c39f6f892b | |||
| f13b51cf91 | |||
| e90c60c940 | |||
| 60029c2a8b | |||
| 14429d449c | |||
| cba9d6078c | |||
| 2dfad98ed6 | |||
| 9de9cac23a | |||
| d1fd7c3f6e | |||
| 8fd5b93127 | |||
| dafa5d4bac | |||
| d29cd5b9d9 | |||
| 60094c6581 | |||
| dd32e228ce | |||
| 81639c61a8 | |||
| ae1fcbcffb | |||
| 263d035181 | |||
| 8ecd7011cb | |||
| f7d42ba60d | |||
| 701cf47c4b | |||
| 315009a7f0 | |||
| 6e98a369de | |||
| fa69b7017b | |||
| e98f3cfedf | |||
| 5f4184536a | |||
| ac904c60c8 | |||
| 16defdbfdf | |||
| d010a050d3 | |||
| 7b541ff958 | |||
| 78a47920ef | |||
| e5a2011a85 | |||
| 640cab2ca8 | |||
| deb8bde078 | |||
| 79f0271720 | |||
| 5b8b80cf59 | |||
| a7ee45a0ba | |||
| 37cc949d94 | |||
| 5f59e1435e | |||
| e1f26aeb4e | |||
| b49dafc412 | |||
| 7c84059b3a | |||
| 10a0bf386b | |||
| 2936cb425d | |||
| e5a9c65e66 | |||
| 81fdff1039 | |||
| 5101b1767b | |||
| e90aa6abda | |||
| 1dd56df5d8 | |||
| 9bd8b7acf5 | |||
| 2a5a422079 | |||
| e92afb74c6 | |||
| 4421e54cde | |||
| b5938985f2 | |||
| 0601f15bd0 | |||
| 66020d250c | |||
| 909286c0f7 | |||
| 5efad94d3d | |||
| 1e24653e79 | |||
| c9bc2b880a | |||
| 545cd3a994 | |||
| 51a09585db | |||
| b98816635d | |||
| f6ed609134 | |||
| aec2dc5bb0 | |||
| 15c836c566 | |||
| 2ebc5374f4 |
@@ -1,5 +1,10 @@
|
|||||||
<!-- Thank you for sending your pull request. But first, have you included
|
<!-- Thank you for sending your pull request. But first, have you included
|
||||||
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
Did you use AI to create your changes?
|
||||||
|
If so, please state it clearly in the PR description (failing to do so may result in your PR being closed).
|
||||||
|
|
||||||
|
Also, please do a self review of the changes made before submitting the PR to make sure only relevant changes are included.
|
||||||
-->
|
-->
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ updates:
|
|||||||
mkdocs:
|
mkdocs:
|
||||||
patterns:
|
patterns:
|
||||||
- "mkdocs*"
|
- "mkdocs*"
|
||||||
|
scipy:
|
||||||
|
patterns:
|
||||||
|
- "scipy"
|
||||||
|
- "scipy-stubs"
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
environment:
|
environment:
|
||||||
name: develop
|
name: develop
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
+27
-70
@@ -25,10 +25,10 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ "ubuntu-22.04", "ubuntu-24.04" ]
|
os: [ "ubuntu-22.04", "ubuntu-24.04" ]
|
||||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
python-version: ["3.11", "3.12", "3.13"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -47,24 +47,9 @@ jobs:
|
|||||||
cache-suffix: "${{ matrix.python-version }}"
|
cache-suffix: "${{ matrix.python-version }}"
|
||||||
prune-cache: false
|
prune-cache: false
|
||||||
|
|
||||||
- name: Cache_dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: ~/dependencies/
|
|
||||||
key: ${{ runner.os }}-dependencies
|
|
||||||
|
|
||||||
- name: TA binary *nix
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
|
||||||
|
|
||||||
- name: Installation - *nix
|
- name: Installation - *nix
|
||||||
run: |
|
run: |
|
||||||
uv pip install --upgrade wheel
|
uv pip install --upgrade wheel
|
||||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
|
||||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
|
||||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
|
||||||
uv pip install -r requirements-dev.txt
|
uv pip install -r requirements-dev.txt
|
||||||
uv pip install -e ft_client/
|
uv pip install -e ft_client/
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
@@ -160,10 +145,10 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ "macos-14", "macos-15" ]
|
os: [ "macos-14", "macos-15" ]
|
||||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
python-version: ["3.11", "3.12", "3.13"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -174,7 +159,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -183,18 +168,6 @@ jobs:
|
|||||||
cache-suffix: "${{ matrix.python-version }}"
|
cache-suffix: "${{ matrix.python-version }}"
|
||||||
prune-cache: false
|
prune-cache: false
|
||||||
|
|
||||||
- name: Cache_dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: ~/dependencies/
|
|
||||||
key: ${{ matrix.os }}-dependencies
|
|
||||||
|
|
||||||
- name: TA binary *nix
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
|
||||||
|
|
||||||
- name: Installation - macOS (Brew)
|
- name: Installation - macOS (Brew)
|
||||||
run: |
|
run: |
|
||||||
# brew update
|
# brew update
|
||||||
@@ -222,9 +195,6 @@ jobs:
|
|||||||
- name: Installation (python)
|
- name: Installation (python)
|
||||||
run: |
|
run: |
|
||||||
uv pip install wheel
|
uv pip install wheel
|
||||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
|
||||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
|
||||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
|
||||||
uv pip install -r requirements-dev.txt
|
uv pip install -r requirements-dev.txt
|
||||||
uv pip install -e ft_client/
|
uv pip install -e ft_client/
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
@@ -287,11 +257,11 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ windows-latest ]
|
os: [ "windows-2022", "windows-2025" ]
|
||||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
python-version: ["3.11", "3.12", "3.13"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -301,7 +271,7 @@ jobs:
|
|||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -315,7 +285,9 @@ jobs:
|
|||||||
function uvpipFunction { uv pip $args }
|
function uvpipFunction { uv pip $args }
|
||||||
Set-Alias -name pip -value uvpipFunction
|
Set-Alias -name pip -value uvpipFunction
|
||||||
|
|
||||||
./build_helpers/install_windows.ps1
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: |
|
run: |
|
||||||
@@ -379,7 +351,7 @@ jobs:
|
|||||||
mypy-version-check:
|
mypy-version-check:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -396,7 +368,7 @@ jobs:
|
|||||||
pre-commit:
|
pre-commit:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -408,7 +380,7 @@ jobs:
|
|||||||
docs-check:
|
docs-check:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -439,7 +411,7 @@ jobs:
|
|||||||
# Run pytest with "live" checks
|
# Run pytest with "live" checks
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -449,7 +421,7 @@ jobs:
|
|||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -458,25 +430,9 @@ jobs:
|
|||||||
cache-suffix: "3.12"
|
cache-suffix: "3.12"
|
||||||
prune-cache: false
|
prune-cache: false
|
||||||
|
|
||||||
- name: Cache_dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: ~/dependencies/
|
|
||||||
key: ${{ runner.os }}-dependencies
|
|
||||||
|
|
||||||
|
|
||||||
- name: TA binary *nix
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
|
||||||
|
|
||||||
- name: Installation - *nix
|
- name: Installation - *nix
|
||||||
run: |
|
run: |
|
||||||
uv pip install --upgrade wheel
|
uv pip install --upgrade wheel
|
||||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
|
||||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
|
||||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
|
||||||
uv pip install -r requirements-dev.txt
|
uv pip install -r requirements-dev.txt
|
||||||
uv pip install -e ft_client/
|
uv pip install -e ft_client/
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
@@ -508,15 +464,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Check user permission
|
- name: Check user permission
|
||||||
id: check
|
id: check
|
||||||
uses: scherermichael-oss/action-has-permission@136e061bfe093832d87f090dd768e14e27a740d3 # 1.0.6
|
continue-on-error: true
|
||||||
|
uses: prince-chrismc/check-actor-permissions-action@d504e74ba31658f4cdf4fcfeb509d4c09736d88e # v3.0.2
|
||||||
with:
|
with:
|
||||||
required-permission: write
|
permission: "write"
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
||||||
if: always() && steps.check.outputs.has-permission && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: steps.check.outputs.permitted == 'true' && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
severity: info
|
severity: info
|
||||||
details: Test Completed!
|
details: Test Completed!
|
||||||
@@ -528,7 +485,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -575,12 +532,12 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download artifact 📦
|
- name: Download artifact 📦
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: freqtrade*-build
|
pattern: freqtrade*-build
|
||||||
path: dist
|
path: dist
|
||||||
@@ -604,12 +561,12 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download artifact 📦
|
- name: Download artifact 📦
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: freqtrade*-build
|
pattern: freqtrade*-build
|
||||||
path: dist
|
path: dist
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
name: Deploy Docs through mike
|
name: Deploy Docs through mike
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy-docker:
|
deploy-docker:
|
||||||
|
name: "Deploy Docker x64 and armv7l"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
if: github.repository == 'freqtrade/freqtrade'
|
if: github.repository == 'freqtrade/freqtrade'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -55,14 +56,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
|
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
|
||||||
|
|
||||||
# We need docker experimental to pull the ARM image.
|
|
||||||
- name: Switch docker to experimental
|
|
||||||
run: |
|
|
||||||
docker version -f '{{.Server.Experimental}}'
|
|
||||||
echo $'{\n "experimental": true\n}' | sudo tee /etc/docker/daemon.json
|
|
||||||
sudo systemctl restart docker
|
|
||||||
docker version -f '{{.Server.Experimental}}'
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||||
|
|
||||||
@@ -82,7 +75,7 @@ jobs:
|
|||||||
build_helpers/publish_docker_multi.sh
|
build_helpers/publish_docker_multi.sh
|
||||||
|
|
||||||
deploy-arm:
|
deploy-arm:
|
||||||
name: "Deploy Docker"
|
name: "Deploy Docker ARM64"
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
needs: [ deploy-docker ]
|
needs: [ deploy-docker ]
|
||||||
@@ -91,7 +84,7 @@ jobs:
|
|||||||
if: github.repository == 'freqtrade/freqtrade'
|
if: github.repository == 'freqtrade/freqtrade'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
dockerHubDescription:
|
dockerHubDescription:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
auto-update:
|
auto-update:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: GitHub Actions Security Analysis with zizmor 🌈
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- stable
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- stable
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
zizmor:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
# contents: read # only needed for private repos
|
||||||
|
# actions: read # only needed for private repos
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v4.2.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run zizmor 🌈
|
||||||
|
uses: zizmorcore/zizmor-action@5ca5fc7a4779c5263a3ffa0e1f693009994446d1 # v0.1.2
|
||||||
+10
-9
@@ -21,17 +21,18 @@ repos:
|
|||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v1.16.1"
|
rev: "v1.17.1"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==6.0.0.20250525
|
- types-cachetools==6.1.0.20250717
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.32.4.20250611
|
- types-requests==2.32.4.20250809
|
||||||
- types-tabulate==0.9.0.20241207
|
- types-tabulate==0.9.0.20241207
|
||||||
- types-python-dateutil==2.9.0.20250516
|
- types-python-dateutil==2.9.0.20250822
|
||||||
- SQLAlchemy==2.0.41
|
- scipy-stubs==1.16.1.1
|
||||||
|
- SQLAlchemy==2.0.43
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
@@ -43,13 +44,13 @@ repos:
|
|||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.12.1'
|
rev: 'v0.12.10'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v5.0.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: |
|
exclude: |
|
||||||
@@ -69,7 +70,7 @@ repos:
|
|||||||
)$
|
)$
|
||||||
|
|
||||||
- repo: https://github.com/stefmolin/exif-stripper
|
- repo: https://github.com/stefmolin/exif-stripper
|
||||||
rev: 1.0.0
|
rev: 1.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: strip-exif
|
- id: strip-exif
|
||||||
|
|
||||||
@@ -82,6 +83,6 @@ repos:
|
|||||||
|
|
||||||
# Ensure github actions remain safe
|
# Ensure github actions remain safe
|
||||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||||
rev: v1.11.0
|
rev: v1.12.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: zizmor
|
- id: zizmor
|
||||||
|
|||||||
+9
-14
@@ -1,10 +1,10 @@
|
|||||||
FROM python:3.13.5-slim-bookworm as base
|
FROM python:3.13.7-slim-bookworm AS base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
ENV LC_ALL C.UTF-8
|
ENV LC_ALL=C.UTF-8
|
||||||
ENV PYTHONDONTWRITEBYTECODE 1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV PYTHONFAULTHANDLER 1
|
ENV PYTHONFAULTHANDLER=1
|
||||||
ENV PATH=/home/ftuser/.local/bin:$PATH
|
ENV PATH=/home/ftuser/.local/bin:$PATH
|
||||||
ENV FT_APP_ENV="docker"
|
ENV FT_APP_ENV="docker"
|
||||||
|
|
||||||
@@ -21,17 +21,12 @@ RUN mkdir /freqtrade \
|
|||||||
WORKDIR /freqtrade
|
WORKDIR /freqtrade
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
FROM base as python-deps
|
FROM base AS python-deps
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& pip install --upgrade pip wheel
|
&& pip install --upgrade pip wheel
|
||||||
|
|
||||||
# Install TA-lib
|
|
||||||
COPY build_helpers/* /tmp/
|
|
||||||
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
|
|
||||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
|
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
|
||||||
USER ftuser
|
USER ftuser
|
||||||
@@ -39,9 +34,9 @@ RUN pip install --user --no-cache-dir "numpy<3.0" \
|
|||||||
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt
|
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt
|
||||||
|
|
||||||
# Copy dependencies to runtime-image
|
# Copy dependencies to runtime-image
|
||||||
FROM base as runtime-image
|
FROM base AS runtime-image
|
||||||
COPY --from=python-deps /usr/local/lib /usr/local/lib
|
COPY --from=python-deps /usr/local/lib /usr/local/lib
|
||||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
ENV LD_LIBRARY_PATH=/usr/local/lib
|
||||||
|
|
||||||
COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local
|
COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local
|
||||||
|
|
||||||
@@ -49,7 +44,7 @@ USER ftuser
|
|||||||
# Install and execute
|
# Install and execute
|
||||||
COPY --chown=ftuser:ftuser . /freqtrade/
|
COPY --chown=ftuser:ftuser . /freqtrade/
|
||||||
|
|
||||||
RUN pip install -e . --user --no-cache-dir --no-build-isolation \
|
RUN pip install -e . --user --no-cache-dir \
|
||||||
&& mkdir /freqtrade/user_data/ \
|
&& mkdir /freqtrade/user_data/ \
|
||||||
&& freqtrade install-ui
|
&& freqtrade install-ui
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
[](https://doi.org/10.21105/joss.04864)
|
[](https://doi.org/10.21105/joss.04864)
|
||||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||||
[](https://www.freqtrade.io)
|
[](https://www.freqtrade.io)
|
||||||
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
|
||||||
|
|
||||||
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [x] **Based on Python 3.10+**: For botting on any operating system - Windows, macOS and Linux.
|
- [x] **Based on Python 3.11+**: For botting on any operating system - Windows, macOS and Linux.
|
||||||
- [x] **Persistence**: Persistence is achieved through sqlite.
|
- [x] **Persistence**: Persistence is achieved through sqlite.
|
||||||
- [x] **Dry-run**: Run the bot without paying money.
|
- [x] **Dry-run**: Run the bot without paying money.
|
||||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||||
@@ -146,6 +145,8 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
|
|||||||
- `/stopentry`: Stop entering new trades.
|
- `/stopentry`: Stop entering new trades.
|
||||||
- `/status <trade_id>|[table]`: Lists all or specific open trades.
|
- `/status <trade_id>|[table]`: Lists all or specific open trades.
|
||||||
- `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days.
|
- `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days.
|
||||||
|
- `/profit_long [<n>]`: Lists cumulative profit from all finished long trades, over the last n days.
|
||||||
|
- `/profit_short [<n>]`: Lists cumulative profit from all finished short trades, over the last n days.
|
||||||
- `/forceexit <trade_id>|all`: Instantly exits the given trade (Ignoring `minimum_roi`).
|
- `/forceexit <trade_id>|all`: Instantly exits the given trade (Ignoring `minimum_roi`).
|
||||||
- `/fx <trade_id>|all`: Alias to `/forceexit`
|
- `/fx <trade_id>|all`: Alias to `/forceexit`
|
||||||
- `/performance`: Show performance of each finished trade grouped by pair
|
- `/performance`: Show performance of each finished trade grouped by pair
|
||||||
@@ -154,6 +155,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
|
|||||||
- `/help`: Show help message.
|
- `/help`: Show help message.
|
||||||
- `/version`: Show version.
|
- `/version`: Show version.
|
||||||
|
|
||||||
|
|
||||||
## Development branches
|
## Development branches
|
||||||
|
|
||||||
The project is currently setup in two main branches:
|
The project is currently setup in two main branches:
|
||||||
@@ -219,7 +221,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
|||||||
|
|
||||||
### Software requirements
|
### Software requirements
|
||||||
|
|
||||||
- [Python >= 3.10](http://docs.python-guide.org/en/latest/starting/installation/)
|
- [Python >= 3.11](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
- [pip](https://pip.pypa.io/en/stable/installing/)
|
- [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
|
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
if [ -z "$1" ]; then
|
|
||||||
INSTALL_LOC=/usr/local
|
|
||||||
else
|
|
||||||
INSTALL_LOC=${1}
|
|
||||||
fi
|
|
||||||
echo "Installing to ${INSTALL_LOC}"
|
|
||||||
if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
|
||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
|
||||||
cd ta-lib \
|
|
||||||
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
|
||||||
&& echo "Downloading gcc config.guess and config.sub" \
|
|
||||||
&& curl -s 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \
|
|
||||||
&& curl -s 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \
|
|
||||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
|
||||||
&& make
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Failed building ta-lib."
|
|
||||||
cd .. && rm -rf ./ta-lib/
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ -z "$2" ]; then
|
|
||||||
which sudo && sudo make install || make install
|
|
||||||
if [ -x "$(command -v apt-get)" ]; then
|
|
||||||
echo "Updating library path using ldconfig"
|
|
||||||
sudo ldconfig
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Don't install with sudo
|
|
||||||
make install
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd .. && rm -rf ./ta-lib/
|
|
||||||
else
|
|
||||||
echo "TA-lib already installed, skipping installation"
|
|
||||||
fi
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
|
|
||||||
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
|
||||||
|
|
||||||
pip install -U wheel "numpy<3.0"
|
|
||||||
pip install --only-binary ta-lib --find-links=build_helpers\ ta-lib
|
|
||||||
|
|
||||||
pip install -r requirements-dev.txt
|
|
||||||
pip install -e .
|
|
||||||
@@ -16,10 +16,12 @@ with require_dev.open("r") as rfile:
|
|||||||
with require.open("r") as rfile:
|
with require.open("r") as rfile:
|
||||||
requirements.extend(rfile.readlines())
|
requirements.extend(rfile.readlines())
|
||||||
|
|
||||||
# Extract types only
|
# Extract relevant types only
|
||||||
type_reqs = [
|
supported = ("types-", "SQLAlchemy", "scipy-stubs")
|
||||||
r.strip("\n") for r in requirements if r.startswith("types-") or r.startswith("SQLAlchemy")
|
|
||||||
]
|
# Find relevant dependencies
|
||||||
|
# Only keep the first part of the line up to the first space
|
||||||
|
type_reqs = [r.strip("\n").split()[0] for r in requirements if r.startswith(supported)]
|
||||||
|
|
||||||
with pre_commit_file.open("r") as file:
|
with pre_commit_file.open("r") as file:
|
||||||
f = yaml.load(file, Loader=yaml.SafeLoader)
|
f = yaml.load(file, Loader=yaml.SafeLoader)
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -180,6 +180,16 @@
|
|||||||
"description": "Offset for profit exit. \nUsually specified in the strategy and missing in the configuration.",
|
"description": "Offset for profit exit. \nUsually specified in the strategy and missing in the configuration.",
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
"recursive_strategy_search": {
|
||||||
|
"description": "Enable recursive strategy search.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"user_data_dir": {
|
||||||
|
"description": "Path to the user data directory."
|
||||||
|
},
|
||||||
|
"datadir": {
|
||||||
|
"description": "Path to the data directory."
|
||||||
|
},
|
||||||
"fee": {
|
"fee": {
|
||||||
"description": "Trading fee percentage. Can help to simulate slippage in backtesting",
|
"description": "Trading fee percentage. Can help to simulate slippage in backtesting",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
@@ -562,6 +572,7 @@
|
|||||||
"pairlists": {
|
"pairlists": {
|
||||||
"description": "Configuration for pairlists.",
|
"description": "Configuration for pairlists.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10
-10
@@ -1,10 +1,10 @@
|
|||||||
FROM python:3.11.13-slim-bookworm as base
|
FROM python:3.11.13-slim-bookworm AS base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
ENV LC_ALL C.UTF-8
|
ENV LC_ALL=C.UTF-8
|
||||||
ENV PYTHONDONTWRITEBYTECODE 1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV PYTHONFAULTHANDLER 1
|
ENV PYTHONFAULTHANDLER=1
|
||||||
ENV PATH=/home/ftuser/.local/bin:$PATH
|
ENV PATH=/home/ftuser/.local/bin:$PATH
|
||||||
ENV FT_APP_ENV="docker"
|
ENV FT_APP_ENV="docker"
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ RUN mkdir /freqtrade \
|
|||||||
WORKDIR /freqtrade
|
WORKDIR /freqtrade
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
FROM base as python-deps
|
FROM base AS python-deps
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
|
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
@@ -34,14 +34,14 @@ COPY build_helpers/* /tmp/
|
|||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
||||||
USER ftuser
|
USER ftuser
|
||||||
RUN pip install --user --no-cache-dir "numpy<3.0" \
|
RUN pip install --user --prefer-binary --no-cache-dir "numpy<3.0" build \
|
||||||
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib \
|
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib \
|
||||||
&& pip install --user --no-cache-dir -r requirements.txt
|
&& pip install --user --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy dependencies to runtime-image
|
# Copy dependencies to runtime-image
|
||||||
FROM base as runtime-image
|
FROM base AS runtime-image
|
||||||
COPY --from=python-deps /usr/local/lib /usr/local/lib
|
COPY --from=python-deps /usr/local/lib /usr/local/lib
|
||||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
ENV LD_LIBRARY_PATH=/usr/local/lib
|
||||||
|
|
||||||
COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local
|
COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ USER ftuser
|
|||||||
# Install and execute
|
# Install and execute
|
||||||
COPY --chown=ftuser:ftuser . /freqtrade/
|
COPY --chown=ftuser:ftuser . /freqtrade/
|
||||||
|
|
||||||
RUN pip install -e . --user --no-cache-dir --no-build-isolation\
|
RUN pip install -e . --user --no-cache-dir \
|
||||||
&& mkdir /freqtrade/user_data/ \
|
&& mkdir /freqtrade/user_data/ \
|
||||||
&& freqtrade install-ui
|
&& freqtrade install-ui
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
FROM freqtradeorg/freqtrade:develop
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
COPY requirements-dev.txt /freqtrade/
|
|
||||||
|
|
||||||
RUN pip install numpy --user --no-cache-dir \
|
|
||||||
&& pip install -r requirements-dev.txt --user --no-cache-dir
|
|
||||||
|
|
||||||
# Empty the ENTRYPOINT to allow all commands
|
|
||||||
ENTRYPOINT []
|
|
||||||
@@ -46,29 +46,32 @@ ranging from the simplest (0) to the most detailed per pair, per buy and per sel
|
|||||||
|
|
||||||
More options are available by running with the `-h` option.
|
More options are available by running with the `-h` option.
|
||||||
|
|
||||||
### Using export-filename
|
### Using backtest-filename
|
||||||
|
|
||||||
Normally, `backtesting-analysis` uses the latest backtest results, but if you wanted to go
|
By default, `backtesting-analysis` processes the most recent backtest results in the `user_data/backtest_results` directory.
|
||||||
back to a previous backtest output, you need to supply the `--export-filename` option.
|
If you want to analyze results from an earlier backtest, use the `--backtest-filename` option to specify the desired file. This lets you revisit and re-analyze historical backtest outputs at any time by providing the filename of the relevant backtest result:
|
||||||
You can supply the same parameter to `backtest-analysis` with the name of the final backtest
|
|
||||||
output file. This allows you to keep historical versions of backtest results and re-analyse
|
|
||||||
them at a later date:
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade backtesting -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange=<timerange> --export=signals --export-filename=/tmp/mystrat_backtest.json
|
freqtrade backtesting-analysis -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange <timerange> --export signals --backtest-filename backtest-result-2025-03-05_20-38-34.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
You should see some output similar to below in the logs with the name of the timestamped
|
You should see some output similar to below in the logs with the name of the timestamped
|
||||||
filename that was exported:
|
filename that was exported:
|
||||||
|
|
||||||
```
|
```
|
||||||
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "/tmp/mystrat_backtest-2022-06-14_16-28-32.json"
|
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "mystrat_backtest-2022-06-14_16-28-32.json"
|
||||||
```
|
```
|
||||||
|
|
||||||
You can then use that filename in `backtesting-analysis`:
|
You can then use that filename in `backtesting-analysis`:
|
||||||
|
|
||||||
```
|
```
|
||||||
freqtrade backtesting-analysis -c <config.json> --export-filename=/tmp/mystrat_backtest-2022-06-14_16-28-32.json
|
freqtrade backtesting-analysis -c <config.json> --backtest-filename=mystrat_backtest-2022-06-14_16-28-32.json
|
||||||
|
```
|
||||||
|
|
||||||
|
To use a result from a different results directory, you can use `--backtest-directory` to specify the directory
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade backtesting-analysis -c <config.json> --backtest-directory custom_results/ --backtest-filename mystrat_backtest-2022-06-14_16-28-32.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tuning the buy tags and sell tags to display
|
### Tuning the buy tags and sell tags to display
|
||||||
|
|||||||
+237
-203
@@ -105,12 +105,14 @@ Only use this if you're sure you'll not want to plot or analyze your results fur
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Exporting trades to file specifying a custom filename
|
Exporting trades to file specifying a custom directory
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --strategy backtesting --export trades --export-filename=backtest_samplestrategy.json
|
freqtrade backtesting --strategy backtesting --export trades --backtest-directory=user_data/custom-backtest-results
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period).
|
Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -158,117 +160,136 @@ The most important in the backtesting is to understand the result.
|
|||||||
A backtesting result will look like that:
|
A backtesting result will look like that:
|
||||||
|
|
||||||
```
|
```
|
||||||
================================================ BACKTESTING REPORT =================================================
|
BACKTESTING REPORT
|
||||||
| Pair | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% |
|
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|----------+--------+----------------+------------------+----------------+--------------+--------------------------|
|
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||||
| ADA/BTC | 35 | -0.11 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 |
|
┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
| ARK/BTC | 11 | -0.41 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 |
|
│ LTC/USDT:USDT │ 16 │ 1.0 │ 56.176 │ 5.62 │ 16:16:00 │ 16 0 0 100 │
|
||||||
| BTS/BTC | 32 | 0.31 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 |
|
│ ETC/USDT:USDT │ 12 │ 0.72 │ 30.936 │ 3.09 │ 9:55:00 │ 11 0 1 91.7 │
|
||||||
| DASH/BTC | 13 | -0.08 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 |
|
│ ETH/USDT:USDT │ 8 │ 0.66 │ 17.864 │ 1.79 │ 1 day, 13:55:00 │ 7 0 1 87.5 │
|
||||||
| ENG/BTC | 18 | 1.36 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 |
|
│ XLM/USDT:USDT │ 10 │ 0.31 │ 11.054 │ 1.11 │ 12:08:00 │ 9 0 1 90.0 │
|
||||||
| EOS/BTC | 36 | 0.08 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 |
|
│ BTC/USDT:USDT │ 8 │ 0.21 │ 7.289 │ 0.73 │ 3 days, 1:24:00 │ 6 0 2 75.0 │
|
||||||
| ETC/BTC | 26 | 0.37 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 |
|
│ XRP/USDT:USDT │ 9 │ -0.14 │ -7.261 │ -0.73 │ 21:18:00 │ 8 0 1 88.9 │
|
||||||
| ETH/BTC | 33 | 0.30 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 |
|
│ DOT/USDT:USDT │ 6 │ -0.4 │ -9.187 │ -0.92 │ 5:35:00 │ 4 0 2 66.7 │
|
||||||
| IOTA/BTC | 32 | 0.03 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 |
|
│ ADA/USDT:USDT │ 8 │ -1.76 │ -52.098 │ -5.21 │ 11:38:00 │ 6 0 2 75.0 │
|
||||||
| LSK/BTC | 15 | 1.75 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 |
|
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||||
| LTC/BTC | 32 | -0.04 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 |
|
└───────────────┴────────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||||
| NANO/BTC | 17 | 1.26 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 |
|
LEFT OPEN TRADES REPORT
|
||||||
| NEO/BTC | 23 | 0.82 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 |
|
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
| REQ/BTC | 9 | 1.17 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 |
|
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||||
| XLM/BTC | 16 | 1.22 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 |
|
┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
| XMR/BTC | 23 | -0.18 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 |
|
│ BTC/USDT:USDT │ 1 │ -4.14 │ -9.930 │ -0.99 │ 17 days, 8:00:00 │ 0 0 1 0 │
|
||||||
| XRP/BTC | 35 | 0.66 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
│ ETC/USDT:USDT │ 1 │ -4.24 │ -15.365 │ -1.54 │ 10:40:00 │ 0 0 1 0 │
|
||||||
| ZEC/BTC | 22 | -0.46 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
│ DOT/USDT:USDT │ 1 │ -5.29 │ -19.125 │ -1.91 │ 11:30:00 │ 0 0 1 0 │
|
||||||
| TOTAL | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
│ TOTAL │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||||
============================================= LEFT OPEN TRADES REPORT =============================================
|
└───────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────────┴────────────────────────┘
|
||||||
| Pair | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
ENTER TAG STATS
|
||||||
|----------+---------+----------------+------------------+----------------+----------------+---------------------|
|
┏━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
| ADA/BTC | 1 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
┃ Enter Tag ┃ Entries ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||||
| LTC/BTC | 1 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
┡━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
| TOTAL | 2 | 0.78 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
│ OTHER │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||||
==================== EXIT REASON STATS ====================
|
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||||
| Exit Reason | Exits | Wins | Draws | Losses |
|
└───────────┴─────────┴──────────────┴─────────────────┴──────────────┴──────────────┴────────────────────────┘
|
||||||
|--------------------+---------+-------+--------+---------|
|
EXIT REASON STATS
|
||||||
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
┏━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
| stop_loss | 166 | 0 | 0 | 166 |
|
┃ Exit Reason ┃ Exits ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||||
| exit_signal | 56 | 36 | 0 | 20 |
|
┡━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
| force_exit | 2 | 0 | 0 | 2 |
|
│ roi │ 67 │ 1.05 │ 242.179 │ 24.22 │ 15:49:00 │ 67 0 0 100 │
|
||||||
|
│ exit_signal │ 4 │ -2.23 │ -31.217 │ -3.12 │ 1 day, 8:38:00 │ 0 0 4 0 │
|
||||||
|
│ force_exit │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||||
|
│ stop_loss │ 3 │ -10.14 │ -111.768 │ -11.18 │ 1 day, 3:05:00 │ 0 0 3 0 │
|
||||||
|
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||||
|
└─────────────┴───────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||||
|
MIXED TAG STATS
|
||||||
|
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
┃ Enter Tag ┃ Exit Reason ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||||
|
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
|
│ │ roi │ 67 │ 1.05 │ 242.179 │ 24.22 │ 15:49:00 │ 67 0 0 100 │
|
||||||
|
│ │ exit_signal │ 4 │ -2.23 │ -31.217 │ -3.12 │ 1 day, 8:38:00 │ 0 0 4 0 │
|
||||||
|
│ │ force_exit │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||||
|
│ │ stop_loss │ 3 │ -10.14 │ -111.768 │ -11.18 │ 1 day, 3:05:00 │ 0 0 3 0 │
|
||||||
|
│ TOTAL │ │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||||
|
└───────────┴─────────────┴────────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||||
|
SUMMARY METRICS
|
||||||
|
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
┃ Metric ┃ Value ┃
|
||||||
|
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
|
│ Backtesting from │ 2025-07-01 00:00:00 │
|
||||||
|
│ Backtesting to │ 2025-08-01 00:00:00 │
|
||||||
|
│ Trading Mode │ Isolated Futures │
|
||||||
|
│ Max open trades │ 3 │
|
||||||
|
│ │ │
|
||||||
|
│ Total/Daily Avg Trades │ 77 / 2.48 │
|
||||||
|
│ Starting balance │ 1000 USDT │
|
||||||
|
│ Final balance │ 1054.774 USDT │
|
||||||
|
│ Absolute profit │ 54.774 USDT │
|
||||||
|
│ Total profit % │ 5.48% │
|
||||||
|
│ CAGR % │ 87.36% │
|
||||||
|
│ Sortino │ 2.48 │
|
||||||
|
│ Sharpe │ 3.75 │
|
||||||
|
│ Calmar │ 40.99 │
|
||||||
|
│ SQN │ 0.69 │
|
||||||
|
│ Profit factor │ 1.29 │
|
||||||
|
│ Expectancy (Ratio) │ 0.71 (0.04) │
|
||||||
|
│ Avg. daily profit │ 1.767 USDT │
|
||||||
|
│ Avg. stake amount │ 345.016 USDT │
|
||||||
|
│ Total trade volume │ 53316.954 USDT │
|
||||||
|
│ │ │
|
||||||
|
│ Long / Short trades │ 67 / 10 │
|
||||||
|
│ Long / Short profit % │ 8.94% / -3.47% │
|
||||||
|
│ Long / Short profit USDT │ 89.425 / -34.651 │
|
||||||
|
│ │ │
|
||||||
|
│ Best Pair │ LTC/USDT:USDT 5.62% │
|
||||||
|
│ Worst Pair │ ADA/USDT:USDT -5.21% │
|
||||||
|
│ Best trade │ ETC/USDT:USDT 2.00% │
|
||||||
|
│ Worst trade │ ADA/USDT:USDT -10.17% │
|
||||||
|
│ Best day │ 26.91 USDT │
|
||||||
|
│ Worst day │ -47.741 USDT │
|
||||||
|
│ Days win/draw/lose │ 20 / 6 / 5 │
|
||||||
|
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:49 │
|
||||||
|
│ Min/Max/Avg. Duration Losers │ 0d 10:40 / 17d 08:00 / 2d 17:00 │
|
||||||
|
│ Max Consecutive Wins / Loss │ 36 / 3 │
|
||||||
|
│ Rejected Entry signals │ 258 │
|
||||||
|
│ Entry/Exit Timeouts │ 0 / 0 │
|
||||||
|
│ │ │
|
||||||
|
│ Min balance │ 1003.168 USDT │
|
||||||
|
│ Max balance │ 1149.421 USDT │
|
||||||
|
│ Max % of account underwater │ 8.23% │
|
||||||
|
│ Absolute drawdown │ 94.647 USDT (8.23%) │
|
||||||
|
│ Drawdown duration │ 9 days 08:50:00 │
|
||||||
|
│ Profit at drawdown start │ 149.421 USDT │
|
||||||
|
│ Profit at drawdown end │ 54.774 USDT │
|
||||||
|
│ Drawdown start │ 2025-07-22 15:10:00 │
|
||||||
|
│ Drawdown end │ 2025-08-01 00:00:00 │
|
||||||
|
│ Market change │ 30.51% │
|
||||||
|
└───────────────────────────────┴─────────────────────────────────┘
|
||||||
|
|
||||||
================== SUMMARY METRICS ==================
|
Backtested 2025-07-01 00:00:00 -> 2025-08-01 00:00:00 | Max open trades : 3
|
||||||
| Metric | Value |
|
STRATEGY SUMMARY
|
||||||
|-----------------------------+---------------------|
|
┏━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
|
||||||
| Backtesting from | 2019-01-01 00:00:00 |
|
┃ Strategy ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃ Drawdown ┃
|
||||||
| Backtesting to | 2019-05-01 00:00:00 |
|
┡━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
|
||||||
| Trading Mode | Spot |
|
│ SampleStrategy │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │ 94.647 USDT 8.23% │
|
||||||
| Max open trades | 3 |
|
└────────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────┴────────────────────────┴────────────────────┘
|
||||||
| | |
|
|
||||||
| Total/Daily Avg Trades | 429 / 3.575 |
|
|
||||||
| Starting balance | 0.01000000 BTC |
|
|
||||||
| Final balance | 0.01762792 BTC |
|
|
||||||
| Absolute profit | 0.00762792 BTC |
|
|
||||||
| Total profit % | 76.2% |
|
|
||||||
| CAGR % | 460.87% |
|
|
||||||
| 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 |
|
|
||||||
| Total trade volume | 0.429 BTC |
|
|
||||||
| | |
|
|
||||||
| Long / Short | 352 / 77 |
|
|
||||||
| Total profit Long % | 1250.58% |
|
|
||||||
| Total profit Short % | -15.02% |
|
|
||||||
| Absolute profit Long | 0.00838792 BTC |
|
|
||||||
| Absolute profit Short | -0.00076 BTC |
|
|
||||||
| | |
|
|
||||||
| Best Pair | LSK/BTC 26.26% |
|
|
||||||
| Worst Pair | ZEC/BTC -10.18% |
|
|
||||||
| Best Trade | LSK/BTC 4.25% |
|
|
||||||
| Worst Trade | ZEC/BTC -10.25% |
|
|
||||||
| Best day | 0.00076 BTC |
|
|
||||||
| Worst day | -0.00036 BTC |
|
|
||||||
| Days win/draw/lose | 12 / 82 / 25 |
|
|
||||||
| Avg. Duration Winners | 4:23:00 |
|
|
||||||
| Avg. Duration Loser | 6:55:00 |
|
|
||||||
| Max Consecutive Wins / Loss | 3 / 4 |
|
|
||||||
| Rejected Entry signals | 3089 |
|
|
||||||
| Entry/Exit Timeouts | 0 / 0 |
|
|
||||||
| Canceled Trade Entries | 34 |
|
|
||||||
| Canceled Entry Orders | 123 |
|
|
||||||
| Replaced Entry Orders | 89 |
|
|
||||||
| | |
|
|
||||||
| Min balance | 0.00945123 BTC |
|
|
||||||
| Max balance | 0.01846651 BTC |
|
|
||||||
| Max % of account underwater | 25.19% |
|
|
||||||
| Absolute Drawdown (Account) | 13.33% |
|
|
||||||
| Drawdown | 0.0015 BTC |
|
|
||||||
| Drawdown high | 0.0013 BTC |
|
|
||||||
| Drawdown low | -0.0002 BTC |
|
|
||||||
| Drawdown Start | 2019-02-15 14:10:00 |
|
|
||||||
| Drawdown End | 2019-04-11 18:15:00 |
|
|
||||||
| Market change | -5.88% |
|
|
||||||
=====================================================
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backtesting report table
|
### Backtesting report table
|
||||||
|
|
||||||
The 1st table contains all trades the bot made, including "left open trades".
|
The first table contains all trades the bot made, including "left open trades".
|
||||||
|
|
||||||
The last line will give you the overall performance of your strategy,
|
The last line will give you the overall performance of your strategy,
|
||||||
here:
|
here:
|
||||||
|
|
||||||
```
|
```
|
||||||
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||||
```
|
```
|
||||||
|
|
||||||
The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has
|
The bot has made `77` trades for an average duration of `22:12:00`, with a performance of `5.48%` (profit), that means it has earned a total of `54.774 USDT` starting with a capital of 1000 USDT.
|
||||||
earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
|
|
||||||
|
|
||||||
The column `Avg Profit %` shows the average profit for all trades made.
|
The column `Avg Profit %` shows the average profit for all trades made.
|
||||||
The column `Tot Profit %` shows instead the total profit % in relation to the starting balance.
|
The column `Tot Profit %` shows instead the total profit % in relation to the starting balance.
|
||||||
In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`.
|
|
||||||
|
In the above results, we have a starting balance of 1000 USDT and an absolute profit of 54.774 USDT - so the `Tot Profit %` will be `(54.774 / 1000) * 100 ~= 5.48%`.
|
||||||
|
|
||||||
Your strategy performance is influenced by your entry strategy, your exit strategy, and also by the `minimal_roi` and `stop_loss` you have set.
|
Your strategy performance is influenced by your entry strategy, your exit strategy, and also by the `minimal_roi` and `stop_loss` you have set.
|
||||||
|
|
||||||
@@ -284,85 +305,83 @@ On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
|
|||||||
(55%), there is almost no chance that the bot will ever reach this profit.
|
(55%), there is almost no chance that the bot will ever reach this profit.
|
||||||
Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up.
|
Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up.
|
||||||
|
|
||||||
### Exit reasons table
|
|
||||||
|
|
||||||
The 2nd table contains a recap of exit reasons.
|
|
||||||
This table can tell you which area needs some additional work (e.g. all or many of the `exit_signal` trades are losses, so you should work on improving the exit signal, or consider disabling it).
|
|
||||||
|
|
||||||
### Left open trades table
|
### Left open trades table
|
||||||
|
|
||||||
The 3rd table contains all trades the bot had to `force_exit` at the end of the backtesting period to present you the full picture.
|
The second table contains all trades the bot had to `force_exit` at the end of the backtesting period to present you the full picture.
|
||||||
This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever.
|
This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever.
|
||||||
These trades are also included in the first table, but are also shown separately in this table for clarity.
|
These trades are also included in the first table, but are also shown separately in this table for clarity.
|
||||||
|
|
||||||
|
### Enter tag stats table
|
||||||
|
|
||||||
|
The third table provides a breakdown of trades by their entry tags (e.g., `enter_long`, `enter_short`), showing the number of entries, average profit percentage, total profit in the stake currency, total profit percentage, average duration, and the number of wins, draws, and losses for each tag.
|
||||||
|
|
||||||
|
### Exit reason stats table
|
||||||
|
|
||||||
|
The fourth table contains a recap of exit reasons (e.g., `exit_signal`, `roi`, `stop_loss`, `force_exit`). This table can tell you which area needs additional work (e.g., if many `exit_signal` trades are losses, you should work on improving the exit signal or consider disabling it).
|
||||||
|
|
||||||
|
### Mixed tag stats table
|
||||||
|
|
||||||
|
The fifth table combines entry tags and exit reasons, providing a detailed view of how different entry tags performed with specific exit reasons. This can help identify which combinations of entry and exit strategies are most effective.
|
||||||
|
|
||||||
### Summary metrics
|
### Summary metrics
|
||||||
|
|
||||||
The last element of the backtest report is the summary metrics table.
|
The last element of the backtest report is the summary metrics table.
|
||||||
It contains some useful key metrics about performance of your strategy on backtesting data.
|
It contains key metrics about the performance of your strategy on backtesting data.
|
||||||
|
|
||||||
```
|
```
|
||||||
================== SUMMARY METRICS ==================
|
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
| Metric | Value |
|
┃ Metric ┃ Value ┃
|
||||||
|-----------------------------+---------------------|
|
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
| Backtesting from | 2019-01-01 00:00:00 |
|
│ Backtesting from │ 2025-07-01 00:00:00 │
|
||||||
| Backtesting to | 2019-05-01 00:00:00 |
|
│ Backtesting to │ 2025-08-01 00:00:00 │
|
||||||
| Trading Mode | Spot |
|
│ Trading Mode │ Isolated Futures │
|
||||||
| Max open trades | 3 |
|
│ Max open trades │ 3 │
|
||||||
| | |
|
│ │ │
|
||||||
| Total/Daily Avg Trades | 429 / 3.575 |
|
│ Total/Daily Avg Trades │ 72 / 2.32 │
|
||||||
| Starting balance | 0.01000000 BTC |
|
│ Starting balance │ 1000 USDT │
|
||||||
| Final balance | 0.01762792 BTC |
|
│ Final balance │ 1106.734 USDT │
|
||||||
| Absolute profit | 0.00762792 BTC |
|
│ Absolute profit │ 106.734 USDT │
|
||||||
| Total profit % | 76.2% |
|
│ Total profit % │ 10.67% │
|
||||||
| CAGR % | 460.87% |
|
│ CAGR % │ 230.04% │
|
||||||
| Sortino | 1.88 |
|
│ Sortino │ 4.99 │
|
||||||
| Sharpe | 2.97 |
|
│ Sharpe │ 8.00 │
|
||||||
| Calmar | 6.29 |
|
│ Calmar │ 77.76 │
|
||||||
| SQN | 2.45 |
|
│ SQN │ 1.52 │
|
||||||
| Profit factor | 1.11 |
|
│ Profit factor │ 1.79 │
|
||||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
│ Expectancy (Ratio) │ 1.48 (0.07) │
|
||||||
| Avg. stake amount | 0.001 BTC |
|
│ Avg. daily profit │ 3.443 USDT │
|
||||||
| Total trade volume | 0.429 BTC |
|
│ Avg. stake amount │ 363.133 USDT │
|
||||||
| | |
|
│ Total trade volume │ 52466.174 USDT │
|
||||||
| Long / Short | 352 / 77 |
|
│ │ │
|
||||||
| Total profit Long % | 1250.58% |
|
│ Best Pair │ LTC/USDT:USDT 4.48% │
|
||||||
| Total profit Short % | -15.02% |
|
│ Worst Pair │ ADA/USDT:USDT -1.78% │
|
||||||
| Absolute profit Long | 0.00838792 BTC |
|
│ Best trade │ ETC/USDT:USDT 2.00% │
|
||||||
| Absolute profit Short | -0.00076 BTC |
|
│ Worst trade │ ADA/USDT:USDT -10.17% │
|
||||||
| | |
|
│ Best day │ 23.535 USDT │
|
||||||
| Best Pair | LSK/BTC 26.26% |
|
│ Worst day │ -49.813 USDT │
|
||||||
| Worst Pair | ZEC/BTC -10.18% |
|
│ Days win/draw/lose │ 21 / 6 / 4 │
|
||||||
| Best Trade | LSK/BTC 4.25% |
|
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:30 │
|
||||||
| Worst Trade | ZEC/BTC -10.25% |
|
│ Min/Max/Avg. Duration Losers │ 0d 12:00 / 17d 08:00 / 3d 23:28 │
|
||||||
| Best day | 0.00076 BTC |
|
│ Max Consecutive Wins / Loss │ 58 / 4 │
|
||||||
| Worst day | -0.00036 BTC |
|
│ Rejected Entry signals │ 254 │
|
||||||
| Days win/draw/lose | 12 / 82 / 25 |
|
│ Entry/Exit Timeouts │ 0 / 0 │
|
||||||
| Avg. Duration Winners | 4:23:00 |
|
│ │ │
|
||||||
| Avg. Duration Loser | 6:55:00 |
|
│ Min balance │ 1003.168 USDT │
|
||||||
| Max Consecutive Wins / Loss | 3 / 4 |
|
│ Max balance │ 1209 USDT │
|
||||||
| Rejected Entry signals | 3089 |
|
│ Max % of account underwater │ 8.46% │
|
||||||
| Entry/Exit Timeouts | 0 / 0 |
|
│ Absolute drawdown │ 102.266 USDT (8.46%) │
|
||||||
| Canceled Trade Entries | 34 |
|
│ Drawdown duration │ 9 days 08:50:00 │
|
||||||
| Canceled Entry Orders | 123 |
|
│ Profit at drawdown start │ 209 USDT │
|
||||||
| Replaced Entry Orders | 89 |
|
│ Profit at drawdown end │ 106.734 USDT │
|
||||||
| | |
|
│ Drawdown start │ 2025-07-22 15:10:00 │
|
||||||
| Min balance | 0.00945123 BTC |
|
│ Drawdown end │ 2025-08-01 00:00:00 │
|
||||||
| Max balance | 0.01846651 BTC |
|
│ Market change │ 30.51% │
|
||||||
| Max % of account underwater | 25.19% |
|
└───────────────────────────────┴─────────────────────────────────┘
|
||||||
| Absolute Drawdown (Account) | 13.33% |
|
|
||||||
| Drawdown | 0.0015 BTC |
|
|
||||||
| Drawdown high | 0.0013 BTC |
|
|
||||||
| Drawdown low | -0.0002 BTC |
|
|
||||||
| Drawdown Start | 2019-02-15 14:10:00 |
|
|
||||||
| Drawdown End | 2019-04-11 18:15:00 |
|
|
||||||
| Market change | -5.88% |
|
|
||||||
=====================================================
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
|
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
|
||||||
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
|
|
||||||
- `Trading Mode`: Spot or Futures trading.
|
- `Trading Mode`: Spot or Futures trading.
|
||||||
|
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
|
||||||
- `Total/Daily Avg Trades`: Identical to the total trades of the backtest output table / Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
|
- `Total/Daily Avg Trades`: Identical to the total trades of the backtest output table / Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
|
||||||
- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line).
|
- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line).
|
||||||
- `Final balance`: Final balance - starting balance + absolute profit.
|
- `Final balance`: Final balance - starting balance + absolute profit.
|
||||||
@@ -373,56 +392,71 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
- `Sharpe`: Annualized Sharpe ratio.
|
- `Sharpe`: Annualized Sharpe ratio.
|
||||||
- `Calmar`: Annualized Calmar ratio.
|
- `Calmar`: Annualized Calmar ratio.
|
||||||
- `SQN`: System Quality Number (SQN) - by Van Tharp.
|
- `SQN`: System Quality Number (SQN) - by Van Tharp.
|
||||||
- `Profit factor`: profit / loss.
|
- `Profit factor`: Sum of the profits of all winning trades divided by the sum of the losses of all losing trades.
|
||||||
|
- `Expectancy (Ratio)`: Expectancy ratio, which is the average profit or loss per trade. A negative expectancy ratio means that your strategy is not profitable.
|
||||||
|
- `Avg. daily profit`: Average profit per day, calculated as `(Total Profit / Backtest Days)`.
|
||||||
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
- `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.
|
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||||
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Tot Profit %`.
|
- `Long / Short trades`: Split long/short trade counts (only shown when short trades were made).
|
||||||
- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade.
|
- `Long / Short profit %`: Profit percentage for long and short trades (only shown when short trades were made).
|
||||||
|
- `Long / Short profit USDT`: Profit in stake currency for long and short trades (only shown when short trades were made).
|
||||||
|
- `Best Pair` / `Worst Pair`: Best and worst performing pair (based on total profit percentage), and its corresponding `Tot Profit %`.
|
||||||
|
- `Best trade` / `Worst trade`: Biggest single winning trade and biggest single losing trade.
|
||||||
- `Best day` / `Worst day`: Best and worst day based on daily profit.
|
- `Best day` / `Worst day`: Best and worst day based on daily profit.
|
||||||
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
|
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trades).
|
||||||
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
- `Min/Max/Avg. Duration Winners`: Minimum, maximum, and average durations for winning trades.
|
||||||
|
- `Min/Max/Avg. Duration Losers`: Minimum, maximum, and average durations for losing trades.
|
||||||
- `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row.
|
- `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row.
|
||||||
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
|
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
|
||||||
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
||||||
- `Canceled Trade Entries`: Number of trades that have been canceled by user request via `adjust_entry_price`.
|
|
||||||
- `Canceled Entry Orders`: Number of entry orders that have been canceled by user request via `adjust_entry_price`.
|
|
||||||
- `Replaced Entry Orders`: Number of entry orders that have been replaced by user request via `adjust_entry_price`.
|
|
||||||
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
||||||
- `Max % of account underwater`: Maximum percentage your account has decreased from the top since the simulation started.
|
- `Max % of account underwater`: Maximum percentage your account has decreased from the top since the simulation started. Calculated as the maximum of `(Max Balance - Current Balance) / (Max Balance)`.
|
||||||
Calculated as the maximum of `(Max Balance - Current Balance) / (Max Balance)`.
|
- `Absolute drawdown`: Maximum absolute drawdown experienced, including percentage relative to the account calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`..
|
||||||
- `Absolute Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
- `Drawdown duration`: Duration of the largest drawdown period.
|
||||||
- `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point.
|
- `Profit at drawdown start` / `Profit at drawdown end`: Profit at the beginning and end of the largest drawdown period.
|
||||||
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost.
|
- `Drawdown start` / `Drawdown end`: Start and end datetime for the largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
||||||
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
- `Market change`: Change of the market during the backtest period. Calculated as the average of all pairs' changes from the first to the last candle using the "close" column.
|
||||||
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
|
|
||||||
- `Long / Short`: Split long/short values (Only shown when short trades were made).
|
|
||||||
- `Total profit Long %` / `Absolute profit Long`: Profit long trades only (Only shown when short trades were made).
|
|
||||||
- `Total profit Short %` / `Absolute profit Short`: Profit short trades only (Only shown when short trades were made).
|
|
||||||
|
|
||||||
### Daily / Weekly / Monthly breakdown
|
### Daily / Weekly / Monthly / Yearly breakdown
|
||||||
|
|
||||||
You can get an overview over daily / weekly or monthly results by using the `--breakdown <>` switch.
|
You can get an overview over daily, weekly, monthly, or yearly results by using the `--breakdown <>` switch.
|
||||||
|
|
||||||
To visualize daily and weekly breakdowns, you can use the following:
|
To visualize monthly and yearly breakdowns, you can use the following:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade backtesting --strategy MyAwesomeStrategy --breakdown day week
|
freqtrade backtesting --strategy MyAwesomeStrategy --breakdown month year
|
||||||
```
|
```
|
||||||
|
|
||||||
``` output
|
``` output
|
||||||
======================== DAY BREAKDOWN =========================
|
MONTH BREAKDOWN
|
||||||
| Day | Tot Profit USDT | Wins | Draws | Losses |
|
┏━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|------------+-------------------+--------+---------+----------|
|
┃ Month ┃ Trades ┃ Tot Profit USDT ┃ Profit Factor ┃ Win Draw Loss Win% ┃
|
||||||
| 03/07/2021 | 200.0 | 2 | 0 | 0 |
|
┡━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
| 04/07/2021 | -50.31 | 0 | 0 | 2 |
|
│ 31/01/2020 │ 12 │ 44.451 │ 7.28 │ 10 0 2 83.3 │
|
||||||
| 05/07/2021 | 220.611 | 3 | 2 | 0 |
|
│ 29/02/2020 │ 30 │ 45.41 │ 2.36 │ 17 0 13 56.7 │
|
||||||
| 06/07/2021 | 150.974 | 3 | 0 | 2 |
|
│ 31/03/2020 │ 35 │ 142.024 │ 2.42 │ 14 0 21 40.0 │
|
||||||
| 07/07/2021 | -70.193 | 1 | 0 | 2 |
|
│ 30/04/2020 │ 67 │ -23.692 │ 0.81 │ 24 0 43 35.8 │
|
||||||
| 08/07/2021 | 212.413 | 2 | 0 | 3 |
|
...
|
||||||
|
...
|
||||||
|
│ 30/04/2025 │ 203 │ -63.43 │ 0.81 │ 73 0 130 36.0 │
|
||||||
|
│ 31/05/2025 │ 142 │ 104.675 │ 1.28 │ 59 0 83 41.5 │
|
||||||
|
│ 30/06/2025 │ 177 │ -1.014 │ 1.0 │ 85 0 92 48.0 │
|
||||||
|
│ 31/07/2025 │ 155 │ 232.762 │ 1.6 │ 63 0 92 40.6 │
|
||||||
|
└────────────┴────────┴─────────────────┴───────────────┴────────────────────────┘
|
||||||
|
YEAR BREAKDOWN
|
||||||
|
┏━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
┃ Year ┃ Trades ┃ Tot Profit USDT ┃ Profit Factor ┃ Win Draw Loss Win% ┃
|
||||||
|
┡━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
|
│ 31/12/2020 │ 896 │ 868.889 │ 1.46 │ 351 0 545 39.2 │
|
||||||
|
│ 31/12/2021 │ 1778 │ 4487.163 │ 1.93 │ 745 0 1033 41.9 │
|
||||||
|
│ 31/12/2022 │ 1736 │ 938.27 │ 1.27 │ 698 0 1038 40.2 │
|
||||||
|
│ 31/12/2023 │ 1712 │ 1677.126 │ 1.68 │ 670 0 1042 39.1 │
|
||||||
|
│ 31/12/2024 │ 1609 │ 3198.424 │ 2.22 │ 773 0 836 48.0 │
|
||||||
|
│ 31/12/2025 │ 1042 │ 716.174 │ 1.33 │ 420 0 622 40.3 │
|
||||||
|
└────────────┴────────┴─────────────────┴───────────────┴────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
The output will show a table containing the realized absolute Profit (in stake currency) for the given timeperiod, as well as wins, draws and losses that materialized (closed) on this day. Below that there will be a second table for the summarized values of weeks indicated by the date of the closing Sunday. The same would apply to a monthly breakdown indicated by the last day of the month.
|
The output will display tables containing the realized absolute profit (in stake currency) for the selected period, along with additional statistics such as number of trades, profit factor, and distribution of wins, draws, and losses that materialized (closed) on this period.
|
||||||
|
|
||||||
### Backtest result caching
|
### Backtest result caching
|
||||||
|
|
||||||
@@ -446,10 +480,10 @@ For this mode - `--notes "<notes>"` can be used to add notes to the backtest res
|
|||||||
The output file freqtrade produces is a zip file containing the following files:
|
The output file freqtrade produces is a zip file containing the following files:
|
||||||
|
|
||||||
- The backtest report in json format
|
- The backtest report in json format
|
||||||
- the market change data in feather format
|
- The market change data in feather format
|
||||||
- a copy of the strategy file
|
- A copy of the strategy file
|
||||||
- a copy of the strategy parameters (if a parameter file was used)
|
- A copy of the strategy parameters (if a parameter file was used)
|
||||||
- a sanitized copy of the config file
|
- A sanitized copy of the config file
|
||||||
|
|
||||||
This will ensure results are reproducible - under the assumption that the same data is available.
|
This will ensure results are reproducible - under the assumption that the same data is available.
|
||||||
|
|
||||||
@@ -467,7 +501,7 @@ Since backtesting lacks some detailed information about what happens within a ca
|
|||||||
- Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open
|
- Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open
|
||||||
- ROI
|
- ROI
|
||||||
- Exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%)
|
- Exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%)
|
||||||
- Exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit
|
- Exits are never "below the candle", so a ROI of 2% may result in an exit at 2.4% if low was at 2.4% profit
|
||||||
- ROI entries which came into effect on the triggering candle (e.g. `120: 0.02` for 1h candles, from `60: 0.05`) will use the candle's open as exit rate
|
- ROI entries which came into effect on the triggering candle (e.g. `120: 0.02` for 1h candles, from `60: 0.05`) will use the candle's open as exit rate
|
||||||
- Force-exits caused by `<N>=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
|
- Force-exits caused by `<N>=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
|
||||||
- Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price
|
- Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price
|
||||||
@@ -533,7 +567,7 @@ freqtrade backtesting --strategy AwesomeStrategy --timeframe 1h --timeframe-deta
|
|||||||
|
|
||||||
This will load 1h data (the main timeframe) as well as 5m data (detail timeframe) for the selected timerange.
|
This will load 1h data (the main timeframe) as well as 5m data (detail timeframe) for the selected timerange.
|
||||||
The strategy will be analyzed with the 1h timeframe.
|
The strategy will be analyzed with the 1h timeframe.
|
||||||
Candles where activity may take place (there's an active signal, the pair is in a trade) are evaluated at the 5m timeframe.
|
Candles where activity may take place (there's an active signal, the pair is in a trade) are evaluated at the 5m timeframe.
|
||||||
This will allow for a more accurate simulation of intra-candle movements - and can lead to different results, especially on higher timeframes.
|
This will allow for a more accurate simulation of intra-candle movements - and can lead to different results, especially on higher timeframes.
|
||||||
|
|
||||||
Entries will generally still happen at the main candle's open, however freed trade slots may be freed earlier (if the exit signal is triggered on the 5m candle), which can then be used for a new trade of a different pair.
|
Entries will generally still happen at the main candle's open, however freed trade slots may be freed earlier (if the exit signal is triggered on the 5m candle), which can then be used for a new trade of a different pair.
|
||||||
@@ -596,5 +630,5 @@ Detailed output for all strategies one after the other will be available, so mak
|
|||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
Great, your strategy is profitable. What if the bot can give your the optimal parameters to use for your strategy?
|
Great, your strategy is profitable. What if the bot can give you the optimal parameters to use for your strategy?
|
||||||
Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md)
|
Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md)
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
usage: freqtrade backtesting-analysis [-h] [-v] [--no-color] [--logfile FILE]
|
usage: freqtrade backtesting-analysis [-h] [-v] [--no-color] [--logfile FILE]
|
||||||
[-V] [-c PATH] [-d PATH]
|
[-V] [-c PATH] [-d PATH]
|
||||||
[--userdir PATH]
|
[--userdir PATH]
|
||||||
[--export-filename PATH]
|
[--backtest-filename PATH]
|
||||||
|
[--backtest-directory PATH]
|
||||||
[--analysis-groups {0,1,2,3,4,5} [{0,1,2,3,4,5} ...]]
|
[--analysis-groups {0,1,2,3,4,5} [{0,1,2,3,4,5} ...]]
|
||||||
[--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]]
|
[--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]]
|
||||||
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
|
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
|
||||||
@@ -14,10 +15,15 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--no-color] [--logfile FILE]
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--export-filename PATH, --backtest-filename PATH
|
--backtest-filename PATH, --export-filename PATH
|
||||||
Use this filename for backtest results.Requires
|
Use this filename for backtest results.Example:
|
||||||
`--export` to be set as well. Example: `--export-filen
|
`--backtest-
|
||||||
ame=user_data/backtest_results/backtest_today.json`
|
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||||
|
Assumes either `user_data/backtest_results/` or
|
||||||
|
`--export-directory` as base directory.
|
||||||
|
--backtest-directory PATH, --export-directory PATH
|
||||||
|
Directory to use for backtest results. Example:
|
||||||
|
`--export-directory=user_data/backtest_results/`.
|
||||||
--analysis-groups {0,1,2,3,4,5} [{0,1,2,3,4,5} ...]
|
--analysis-groups {0,1,2,3,4,5} [{0,1,2,3,4,5} ...]
|
||||||
grouping output - 0: simple wins/losses by enter tag,
|
grouping output - 0: simple wins/losses by enter tag,
|
||||||
1: by enter_tag, 2: by enter_tag and exit_tag, 3: by
|
1: by enter_tag, 2: by enter_tag and exit_tag, 3: by
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
```
|
```
|
||||||
usage: freqtrade backtesting-show [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
usage: freqtrade backtesting-show [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||||
[-c PATH] [-d PATH] [--userdir PATH]
|
[-c PATH] [-d PATH] [--userdir PATH]
|
||||||
[--export-filename PATH] [--show-pair-list]
|
[--backtest-filename PATH]
|
||||||
|
[--backtest-directory PATH]
|
||||||
|
[--show-pair-list]
|
||||||
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
||||||
|
|
||||||
options:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--export-filename PATH, --backtest-filename PATH
|
--backtest-filename PATH, --export-filename PATH
|
||||||
Use this filename for backtest results.Requires
|
Use this filename for backtest results.Example:
|
||||||
`--export` to be set as well. Example: `--export-filen
|
`--backtest-
|
||||||
ame=user_data/backtest_results/backtest_today.json`
|
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||||
|
Assumes either `user_data/backtest_results/` or
|
||||||
|
`--export-directory` as base directory.
|
||||||
|
--backtest-directory PATH, --export-directory PATH
|
||||||
|
Directory to use for backtest results. Example:
|
||||||
|
`--export-directory=user_data/backtest_results/`.
|
||||||
--show-pair-list Show backtesting pairlist sorted by profit.
|
--show-pair-list Show backtesting pairlist sorted by profit.
|
||||||
--breakdown {day,week,month,year} [{day,week,month,year} ...]
|
--breakdown {day,week,month,year} [{day,week,month,year} ...]
|
||||||
Show backtesting breakdown per [day, week, month,
|
Show backtesting breakdown per [day, week, month,
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ usage: freqtrade backtesting [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
|||||||
[--timeframe-detail TIMEFRAME_DETAIL]
|
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
[--export {none,trades,signals}]
|
[--export {none,trades,signals}]
|
||||||
[--export-filename PATH]
|
[--backtest-filename PATH]
|
||||||
|
[--backtest-directory PATH]
|
||||||
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
||||||
[--cache {none,day,week,month}]
|
[--cache {none,day,week,month}]
|
||||||
[--freqai-backtest-live-models] [--notes TEXT]
|
[--freqai-backtest-live-models] [--notes TEXT]
|
||||||
@@ -61,10 +62,15 @@ options:
|
|||||||
becomes `backtest-data-SampleStrategy.json`
|
becomes `backtest-data-SampleStrategy.json`
|
||||||
--export {none,trades,signals}
|
--export {none,trades,signals}
|
||||||
Export backtest results (default: trades).
|
Export backtest results (default: trades).
|
||||||
--export-filename PATH, --backtest-filename PATH
|
--backtest-filename PATH, --export-filename PATH
|
||||||
Use this filename for backtest results.Requires
|
Use this filename for backtest results.Example:
|
||||||
`--export` to be set as well. Example: `--export-filen
|
`--backtest-
|
||||||
ame=user_data/backtest_results/backtest_today.json`
|
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||||
|
Assumes either `user_data/backtest_results/` or
|
||||||
|
`--export-directory` as base directory.
|
||||||
|
--backtest-directory PATH, --export-directory PATH
|
||||||
|
Directory to use for backtest results. Example:
|
||||||
|
`--export-directory=user_data/backtest_results/`.
|
||||||
--breakdown {day,week,month,year} [{day,week,month,year} ...]
|
--breakdown {day,week,month,year} [{day,week,month,year} ...]
|
||||||
Show backtesting breakdown per [day, week, month,
|
Show backtesting breakdown per [day, week, month,
|
||||||
year].
|
year].
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
```
|
```
|
||||||
usage: freqtrade list-exchanges [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
usage: freqtrade list-exchanges [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||||
[-c PATH] [-d PATH] [--userdir PATH] [-1] [-a]
|
[-c PATH] [-d PATH] [--userdir PATH] [-1] [-a]
|
||||||
|
[--trading-mode {spot,margin,futures}]
|
||||||
|
[--dex-exchanges]
|
||||||
|
|
||||||
options:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-1, --one-column Print output in one column.
|
-1, --one-column Print output in one column.
|
||||||
-a, --all Print all exchanges known to the ccxt library.
|
-a, --all Print all exchanges known to the ccxt library.
|
||||||
|
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
|
||||||
|
Select Trading mode
|
||||||
|
--dex-exchanges Print only DEX exchanges.
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ usage: freqtrade lookahead-analysis [-h] [-v] [--no-color] [--logfile FILE]
|
|||||||
[--timeframe-detail TIMEFRAME_DETAIL]
|
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
[--export {none,trades,signals}]
|
[--export {none,trades,signals}]
|
||||||
[--export-filename PATH]
|
[--backtest-filename PATH]
|
||||||
|
[--backtest-directory PATH]
|
||||||
[--freqai-backtest-live-models]
|
[--freqai-backtest-live-models]
|
||||||
[--minimum-trade-amount INT]
|
[--minimum-trade-amount INT]
|
||||||
[--targeted-trade-amount INT]
|
[--targeted-trade-amount INT]
|
||||||
@@ -60,10 +61,15 @@ options:
|
|||||||
becomes `backtest-data-SampleStrategy.json`
|
becomes `backtest-data-SampleStrategy.json`
|
||||||
--export {none,trades,signals}
|
--export {none,trades,signals}
|
||||||
Export backtest results (default: trades).
|
Export backtest results (default: trades).
|
||||||
--export-filename PATH, --backtest-filename PATH
|
--backtest-filename PATH, --export-filename PATH
|
||||||
Use this filename for backtest results.Requires
|
Use this filename for backtest results.Example:
|
||||||
`--export` to be set as well. Example: `--export-filen
|
`--backtest-
|
||||||
ame=user_data/backtest_results/backtest_today.json`
|
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||||
|
Assumes either `user_data/backtest_results/` or
|
||||||
|
`--export-directory` as base directory.
|
||||||
|
--backtest-directory PATH, --export-directory PATH
|
||||||
|
Directory to use for backtest results. Example:
|
||||||
|
`--export-directory=user_data/backtest_results/`.
|
||||||
--freqai-backtest-live-models
|
--freqai-backtest-live-models
|
||||||
Run backtest with ready models.
|
Run backtest with ready models.
|
||||||
--minimum-trade-amount INT
|
--minimum-trade-amount INT
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ usage: freqtrade plot-dataframe [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
|||||||
[--plot-limit INT] [--db-url PATH]
|
[--plot-limit INT] [--db-url PATH]
|
||||||
[--trade-source {DB,file}]
|
[--trade-source {DB,file}]
|
||||||
[--export {none,trades,signals}]
|
[--export {none,trades,signals}]
|
||||||
[--export-filename PATH]
|
[--backtest-filename PATH]
|
||||||
[--timerange TIMERANGE] [-i TIMEFRAME]
|
[--timerange TIMERANGE] [-i TIMEFRAME]
|
||||||
[--no-trades]
|
[--no-trades]
|
||||||
|
|
||||||
@@ -38,10 +38,12 @@ options:
|
|||||||
(backtest file)) Default: file
|
(backtest file)) Default: file
|
||||||
--export {none,trades,signals}
|
--export {none,trades,signals}
|
||||||
Export backtest results (default: trades).
|
Export backtest results (default: trades).
|
||||||
--export-filename PATH, --backtest-filename PATH
|
--backtest-filename PATH, --export-filename PATH
|
||||||
Use this filename for backtest results.Requires
|
Use this filename for backtest results.Example:
|
||||||
`--export` to be set as well. Example: `--export-filen
|
`--backtest-
|
||||||
ame=user_data/backtest_results/backtest_today.json`
|
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||||
|
Assumes either `user_data/backtest_results/` or
|
||||||
|
`--export-directory` as base directory.
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ usage: freqtrade plot-profit [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
|||||||
[--freqaimodel NAME] [--freqaimodel-path PATH]
|
[--freqaimodel NAME] [--freqaimodel-path PATH]
|
||||||
[-p PAIRS [PAIRS ...]] [--timerange TIMERANGE]
|
[-p PAIRS [PAIRS ...]] [--timerange TIMERANGE]
|
||||||
[--export {none,trades,signals}]
|
[--export {none,trades,signals}]
|
||||||
[--export-filename PATH] [--db-url PATH]
|
[--backtest-filename PATH] [--db-url PATH]
|
||||||
[--trade-source {DB,file}] [-i TIMEFRAME]
|
[--trade-source {DB,file}] [-i TIMEFRAME]
|
||||||
[--auto-open]
|
[--auto-open]
|
||||||
|
|
||||||
@@ -19,10 +19,12 @@ options:
|
|||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
--export {none,trades,signals}
|
--export {none,trades,signals}
|
||||||
Export backtest results (default: trades).
|
Export backtest results (default: trades).
|
||||||
--export-filename PATH, --backtest-filename PATH
|
--backtest-filename PATH, --export-filename PATH
|
||||||
Use this filename for backtest results.Requires
|
Use this filename for backtest results.Example:
|
||||||
`--export` to be set as well. Example: `--export-filen
|
`--backtest-
|
||||||
ame=user_data/backtest_results/backtest_today.json`
|
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||||
|
Assumes either `user_data/backtest_results/` or
|
||||||
|
`--export-directory` as base directory.
|
||||||
--db-url PATH Override trades database URL, this is useful in custom
|
--db-url PATH Override trades database URL, this is useful in custom
|
||||||
deployments (default: `sqlite:///tradesv3.sqlite` for
|
deployments (default: `sqlite:///tradesv3.sqlite` for
|
||||||
Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for
|
Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for
|
||||||
|
|||||||
@@ -566,8 +566,8 @@ Configuration:
|
|||||||
|
|
||||||
### Understand order_time_in_force
|
### Understand order_time_in_force
|
||||||
|
|
||||||
The `order_time_in_force` configuration parameter defines the policy by which the order
|
The `order_time_in_force` configuration parameter defines the policy by which the order is executed on the exchange.
|
||||||
is executed on the exchange. Three commonly used time in force are:
|
Commonly used time in force are:
|
||||||
|
|
||||||
**GTC (Good Till Canceled):**
|
**GTC (Good Till Canceled):**
|
||||||
|
|
||||||
@@ -589,11 +589,13 @@ is automatically cancelled by the exchange.
|
|||||||
Post only order. The order is either placed as a maker order, or it is canceled.
|
Post only order. The order is either placed as a maker order, or it is canceled.
|
||||||
This means the order must be placed on orderbook for at least time in an unfilled state.
|
This means the order must be placed on orderbook for at least time in an unfilled state.
|
||||||
|
|
||||||
|
Please check the [Exchange documentation](exchanges.md) for supported time in force values for your exchange.
|
||||||
|
|
||||||
#### time_in_force config
|
#### time_in_force config
|
||||||
|
|
||||||
The `order_time_in_force` parameter contains a dict with entry and exit time in force policy values.
|
The `order_time_in_force` parameter contains a dict with entry and exit time in force policy values.
|
||||||
This can be set in the configuration file or in the strategy.
|
This can be set in the configuration file or in the strategy.
|
||||||
Values set in the configuration file overwrites values set in the strategy.
|
Values set in the configuration file overwrite values from in the strategy, following the regular [precedence rules](#configuration-option-prevalence).
|
||||||
|
|
||||||
The possible values are: `GTC` (default), `FOK` or `IOC`.
|
The possible values are: `GTC` (default), `FOK` or `IOC`.
|
||||||
|
|
||||||
@@ -605,9 +607,9 @@ The possible values are: `GTC` (default), `FOK` or `IOC`.
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
This is ongoing work. For now, it is supported only for binance, gate and kucoin.
|
|
||||||
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
|
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
|
||||||
|
|
||||||
|
|
||||||
### Fiat conversion
|
### Fiat conversion
|
||||||
|
|
||||||
Freqtrade uses the Coingecko API to convert the coin value to it's corresponding fiat value for the Telegram reports.
|
Freqtrade uses the Coingecko API to convert the coin value to it's corresponding fiat value for the Telegram reports.
|
||||||
|
|||||||
+16
-1
@@ -408,6 +408,22 @@ jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace freqtrade/tem
|
|||||||
jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade/templates/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md
|
jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade/templates/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Backtest documentation results
|
||||||
|
|
||||||
|
To generate backtest outputs, please use the following commands:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
# Assume a dedicated user directory for this output
|
||||||
|
freqtrade create-userdir --userdir user_data_bttest/
|
||||||
|
# set can_short = True
|
||||||
|
sed -i "s/can_short: bool = False/can_short: bool = True/" user_data_bttest/strategies/sample_strategy.py
|
||||||
|
|
||||||
|
freqtrade download-data --timerange 20250625-20250801 --config tests/testdata/config.tests.usdt.json --userdir user_data_bttest/ -t 5m
|
||||||
|
|
||||||
|
freqtrade backtesting --config tests/testdata/config.tests.usdt.json -s SampleStrategy --userdir user_data_bttest/ --cache none --timerange 20250701-20250801
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Continuous integration
|
## Continuous integration
|
||||||
|
|
||||||
This documents some decisions taken for the CI Pipeline.
|
This documents some decisions taken for the CI Pipeline.
|
||||||
@@ -418,7 +434,6 @@ This documents some decisions taken for the CI Pipeline.
|
|||||||
* Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of.
|
* Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of.
|
||||||
* Full docker image rebuilds are run once a week via schedule.
|
* Full docker image rebuilds are run once a week via schedule.
|
||||||
* Deployments run on ubuntu.
|
* Deployments run on ubuntu.
|
||||||
* ta-lib binaries are contained in the build_helpers directory to avoid fails related to external unavailability.
|
|
||||||
* All tests must pass for a PR to be merged to `stable` or `develop`.
|
* All tests must pass for a PR to be merged to `stable` or `develop`.
|
||||||
|
|
||||||
## Creating a release
|
## Creating a release
|
||||||
|
|||||||
+55
-11
@@ -227,7 +227,7 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "FOK" (full-or-cancel) and "IOC" (immediate-or-cancel) settings.
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
@@ -271,7 +271,9 @@ Using the wrong exchange will result in the error "OKX Error 50119: API key does
|
|||||||
## Gate.io
|
## Gate.io
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Gate.io supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange..
|
Gate.io supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||||
|
|
||||||
|
Gate.io supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), and "IOC" (immediate-or-cancel) settings.
|
||||||
|
|
||||||
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
|
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
|
||||||
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
|
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
|
||||||
@@ -286,9 +288,15 @@ Without these permissions, the bot will not start correctly and show errors like
|
|||||||
|
|
||||||
## Bybit
|
## Bybit
|
||||||
|
|
||||||
Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode.
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||||
|
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
||||||
|
|
||||||
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors.
|
Bybit supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "FOK" (full-or-cancel), "IOC" (immediate-or-cancel) and "PO" (Post only) settings.
|
||||||
|
|
||||||
|
Futures trading on bybit is currently supported for isolated futures mode.
|
||||||
|
|
||||||
|
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that manual changes to this setting may result in exceptions and errors.
|
||||||
|
|
||||||
As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well.
|
As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well.
|
||||||
|
|
||||||
@@ -305,11 +313,6 @@ We do strongly recommend to limit all API keys to the IP you're going to use it
|
|||||||
We therefore recommend the usage of one subaccount per bot. This is especially important when using unified accounts.
|
We therefore recommend the usage of one subaccount per bot. This is especially important when using unified accounts.
|
||||||
Other configurations (multiple bots on one account, manual non-bot trades on the bot account) are not supported and may lead to unexpected behavior.
|
Other configurations (multiple bots on one account, manual non-bot trades on the bot account) are not supported and may lead to unexpected behavior.
|
||||||
|
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
|
||||||
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
|
||||||
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
|
||||||
|
|
||||||
## Bitmart
|
## Bitmart
|
||||||
|
|
||||||
Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret.
|
Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret.
|
||||||
@@ -328,6 +331,26 @@ It's therefore required to pass the UID as well.
|
|||||||
!!! Warning "Necessary Verification"
|
!!! Warning "Necessary Verification"
|
||||||
Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification.
|
Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification.
|
||||||
|
|
||||||
|
## Bitget
|
||||||
|
|
||||||
|
Bitget requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"exchange": {
|
||||||
|
"name": "bitget",
|
||||||
|
"key": "your_exchange_key",
|
||||||
|
"secret": "your_exchange_secret",
|
||||||
|
"password": "your_exchange_api_key_password",
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bitget supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "FOK" (full-or-cancel), "IOC" (immediate-or-cancel) and "PO" (Post only) settings.
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Bitget supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
|
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
|
||||||
|
|
||||||
## Hyperliquid
|
## Hyperliquid
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
@@ -339,13 +362,13 @@ This needs to be configured like this:
|
|||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "hyperliquid",
|
"name": "hyperliquid",
|
||||||
"walletAddress": "your_eth_wallet_address",
|
"walletAddress": "your_eth_wallet_address", // This should NOT be your API Wallet Address!
|
||||||
"privateKey": "your_api_private_key",
|
"privateKey": "your_api_private_key",
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
* walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your wallet address, not your API Wallet Address.
|
* walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your main wallet address, not your API Wallet Address.
|
||||||
* privateKey in hex format: `0x<64 hex characters>` - Use the key the API Wallet shows on creation.
|
* privateKey in hex format: `0x<64 hex characters>` - Use the key the API Wallet shows on creation.
|
||||||
|
|
||||||
Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer 2 scaling solution built on top of Ethereum. Hyperliquid uses USDC as quote / collateral. The process of depositing USDC on Hyperliquid requires a couple of steps, see [how to start trading](https://hyperliquid.gitbook.io/hyperliquid-docs/onboarding/how-to-start-trading) for details on what steps are needed.
|
Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer 2 scaling solution built on top of Ethereum. Hyperliquid uses USDC as quote / collateral. The process of depositing USDC on Hyperliquid requires a couple of steps, see [how to start trading](https://hyperliquid.gitbook.io/hyperliquid-docs/onboarding/how-to-start-trading) for details on what steps are needed.
|
||||||
@@ -363,6 +386,27 @@ Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer
|
|||||||
* Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet to trade on Hyperliquid.
|
* Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet to trade on Hyperliquid.
|
||||||
* If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet.
|
* If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet.
|
||||||
|
|
||||||
|
### Hyperliquid Vault / Subaccount
|
||||||
|
|
||||||
|
Hyperliquid allows you to create either a vault or a subaccount.
|
||||||
|
To use these with Freqtrade, you will need to use the following configuration pattern:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"exchange": {
|
||||||
|
"name": "hyperliquid",
|
||||||
|
"walletAddress": "your_vault_address", // Vault or subaccount address
|
||||||
|
"privateKey": "your_api_private_key",
|
||||||
|
"ccxt_config": {
|
||||||
|
"options": {
|
||||||
|
"vaultAddress": "your_vault_address" // Optional, only if you want to use a vault or subaccount
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Your balance and trades will now be used from your vault / subaccount - and no longer from your main account.
|
||||||
|
|
||||||
### Historic Hyperliquid data
|
### Historic Hyperliquid data
|
||||||
|
|
||||||
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
|
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
|
||||||
|
|||||||
@@ -159,6 +159,14 @@ This warning can point to one of the below problems:
|
|||||||
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
|
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
|
||||||
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
|
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
|
||||||
|
|
||||||
|
### I get the message "Couldn't reuse watch for xxx" in the log
|
||||||
|
|
||||||
|
This is an informational message that the bot tried to use candles from the websocket, but the exchange didn't provide the right information.
|
||||||
|
This can happen if there was an interruption to the websocket connection - or if the pair didn't have any trades happen in the timeframe you are using.
|
||||||
|
|
||||||
|
Freqtrade will handle this gracefully by falling back to the REST api.
|
||||||
|
While this makes the iteration slightly slower (due to the REST Api call) - it will not cause any problems to the bot's operation.
|
||||||
|
|
||||||
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
|
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
|
||||||
|
|
||||||
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Gate.io).
|
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Gate.io).
|
||||||
|
|||||||
@@ -389,6 +389,8 @@ The `refresh_period` setting defines the interval (in seconds) at which the mark
|
|||||||
The `categories` setting specifies the [coingecko categories](https://www.coingecko.com/en/categories) from which to select coins from. The default is an empty list `[]`, meaning no category filtering is applied.
|
The `categories` setting specifies the [coingecko categories](https://www.coingecko.com/en/categories) from which to select coins from. The default is an empty list `[]`, meaning no category filtering is applied.
|
||||||
If an incorrect category string is chosen, the plugin will print the available categories from CoinGecko and fail. The category should be the ID of the category, for example, for `https://www.coingecko.com/en/categories/layer-1`, the category ID would be `layer-1`. You can pass multiple categories such as `["layer-1", "meme-token"]` to select from several categories.
|
If an incorrect category string is chosen, the plugin will print the available categories from CoinGecko and fail. The category should be the ID of the category, for example, for `https://www.coingecko.com/en/categories/layer-1`, the category ID would be `layer-1`. You can pass multiple categories such as `["layer-1", "meme-token"]` to select from several categories.
|
||||||
|
|
||||||
|
Coins like 1000PEPE/USDT or KPEPE/USDT:USDT are detected on a best effort basis, with the prefixes `1000` and `K` being used to identify them.
|
||||||
|
|
||||||
!!! Warning "Many categories"
|
!!! 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -3,7 +3,6 @@
|
|||||||
[](https://github.com/freqtrade/freqtrade/actions/)
|
[](https://github.com/freqtrade/freqtrade/actions/)
|
||||||
[](https://doi.org/10.21105/joss.04864)
|
[](https://doi.org/10.21105/joss.04864)
|
||||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||||
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
|
||||||
|
|
||||||
<!-- GitHub action buttons -->
|
<!-- GitHub action buttons -->
|
||||||
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }
|
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }
|
||||||
@@ -87,7 +86,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
|
|||||||
|
|
||||||
Alternatively
|
Alternatively
|
||||||
|
|
||||||
- Python 3.10+
|
- Python 3.11+
|
||||||
- pip (pip3)
|
- pip (pip3)
|
||||||
- git
|
- git
|
||||||
- TA-Lib
|
- TA-Lib
|
||||||
|
|||||||
+4
-44
@@ -24,7 +24,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
|||||||
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
|
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Python3.10 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
|
Python3.11 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
|
||||||
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
|
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
|
||||||
|
|
||||||
!!! Warning "Up-to-date clock"
|
!!! Warning "Up-to-date clock"
|
||||||
@@ -42,11 +42,10 @@ These requirements apply to both [Script Installation](#script-installation) and
|
|||||||
|
|
||||||
### Install guide
|
### Install guide
|
||||||
|
|
||||||
* [Python >= 3.10](http://docs.python-guide.org/en/latest/starting/installation/)
|
* [Python >= 3.11](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
||||||
* [TA-Lib](https://ta-lib.github.io/ta-lib-python/) (install instructions [below](#install-ta-lib))
|
|
||||||
|
|
||||||
### Install code
|
### Install code
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th
|
|||||||
OS Specific steps are listed first, the common section below is necessary for all systems.
|
OS Specific steps are listed first, the common section below is necessary for all systems.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Python3.10 or higher and the corresponding pip are assumed to be available.
|
Python3.11 or higher and the corresponding pip are assumed to be available.
|
||||||
|
|
||||||
=== "Debian/Ubuntu"
|
=== "Debian/Ubuntu"
|
||||||
#### Install necessary dependencies
|
#### Install necessary dependencies
|
||||||
@@ -179,7 +178,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr
|
|||||||
** --install **
|
** --install **
|
||||||
|
|
||||||
With this option, the script will install the bot and most dependencies:
|
With this option, the script will install the bot and most dependencies:
|
||||||
You will need to have git and python3.10+ installed beforehand for this to work.
|
You will need to have git and python3.11+ installed beforehand for this to work.
|
||||||
|
|
||||||
* Mandatory software as: `ta-lib`
|
* Mandatory software as: `ta-lib`
|
||||||
* Setup your virtualenv under `.venv/`
|
* Setup your virtualenv under `.venv/`
|
||||||
@@ -201,35 +200,6 @@ This option will hard reset your branch (only if you are on either `stable` or `
|
|||||||
|
|
||||||
Make sure you fulfill the [Requirements](#requirements) and have downloaded the [Freqtrade repository](#freqtrade-repository).
|
Make sure you fulfill the [Requirements](#requirements) and have downloaded the [Freqtrade repository](#freqtrade-repository).
|
||||||
|
|
||||||
### Install TA-Lib
|
|
||||||
|
|
||||||
#### TA-Lib script installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo ./build_helpers/install_ta-lib.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
This will use the ta-lib tar.gz included in this repository.
|
|
||||||
|
|
||||||
##### TA-Lib manual installation
|
|
||||||
|
|
||||||
[Official installation guide](https://ta-lib.github.io/ta-lib-python/install.html)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
|
||||||
tar xvzf ta-lib-0.4.0-src.tar.gz
|
|
||||||
cd ta-lib
|
|
||||||
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
|
||||||
./configure --prefix=/usr/local
|
|
||||||
make
|
|
||||||
sudo make install
|
|
||||||
# On debian based systems (debian, ubuntu, ...) - updating ldconfig might be necessary.
|
|
||||||
sudo ldconfig
|
|
||||||
cd ..
|
|
||||||
rm -rf ./ta-lib*
|
|
||||||
```
|
|
||||||
|
|
||||||
### Setup Python virtual environment (virtualenv)
|
### Setup Python virtual environment (virtualenv)
|
||||||
|
|
||||||
You will run freqtrade in separated `virtual environment`
|
You will run freqtrade in separated `virtual environment`
|
||||||
@@ -332,16 +302,6 @@ python3 -m pip install -r requirements.txt
|
|||||||
python3 -m pip install -e .
|
python3 -m pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
Patch conda libta-lib (Linux only)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ensure that the environment is active!
|
|
||||||
conda activate freqtrade
|
|
||||||
|
|
||||||
cd build_helpers
|
|
||||||
bash install_ta-lib.sh ${CONDA_PREFIX} nosudo
|
|
||||||
```
|
|
||||||
|
|
||||||
[You are now ready](#you-are-ready) to run the bot.
|
[You are now ready](#you-are-ready) to run the bot.
|
||||||
|
|
||||||
### Important shortcuts
|
### Important shortcuts
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Many strategies, without the programmer knowing, have fallen prey to lookahead b
|
|||||||
This typically makes the strategy backtest look profitable, sometimes to extremes, but this is not realistic as the strategy is "cheating" by looking at data it would not have in dry or live modes.
|
This typically makes the strategy backtest look profitable, sometimes to extremes, but this is not realistic as the strategy is "cheating" by looking at data it would not have in dry or live modes.
|
||||||
|
|
||||||
The reason why strategies can "cheat" is because the freqtrade backtesting process populates the full dataframe including all candle timestamps at the outset.
|
The reason why strategies can "cheat" is because the freqtrade backtesting process populates the full dataframe including all candle timestamps at the outset.
|
||||||
If the programmer is not careful or oblivious how things work internally
|
If the programmer is not careful or oblivious how things work internally
|
||||||
(which sometimes can be really hard to find out) then the strategy will look into the future.
|
(which sometimes can be really hard to find out) then the strategy will look into the future.
|
||||||
|
|
||||||
This command is made to try to verify the validity in the form of the aforementioned lookahead bias.
|
This command is made to try to verify the validity in the form of the aforementioned lookahead bias.
|
||||||
@@ -50,8 +50,7 @@ After this initial backtest runs, it will look if the `minimum-trade-amount` is
|
|||||||
If this happens, use a wider timerange to get more trades for the analysis, or use a timerange where more trades occur.
|
If this happens, use a wider timerange to get more trades for the analysis, or use a timerange where more trades occur.
|
||||||
|
|
||||||
After setting the baseline it will then do additional backtest runs for every entry and exit separately.
|
After setting the baseline it will then do additional backtest runs for every entry and exit separately.
|
||||||
When these verification backtests complete, it will compare the indicators at the signal candles (both entry or exit)
|
When these verification backtests complete, it will compare both dataframes (baseline and sliced) for any difference in columns' value and report the bias.
|
||||||
and report the bias.
|
|
||||||
After all signals have been verified or falsified a result table will be generated for the user to see.
|
After all signals have been verified or falsified a result table will be generated for the user to see.
|
||||||
|
|
||||||
### How to find and remove bias? How can I salvage a biased strategy?
|
### How to find and remove bias? How can I salvage a biased strategy?
|
||||||
@@ -98,8 +97,8 @@ If the strategy has many different signals / signal types, it's up to you to sel
|
|||||||
This would lead to a false-negative, i.e. the strategy will be reported as non-biased.
|
This would lead to a false-negative, i.e. the strategy will be reported as non-biased.
|
||||||
- `lookahead-analysis` has access to the same backtesting options and this can introduce problems.
|
- `lookahead-analysis` has access to the same backtesting options and this can introduce problems.
|
||||||
Please don't use any options like enabling position stacking as this will distort the number of checked signals.
|
Please don't use any options like enabling position stacking as this will distort the number of checked signals.
|
||||||
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` slots,
|
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` slots,
|
||||||
and that you have enough capital in the backtest wallet configuration.
|
and that you have enough capital in the backtest wallet configuration.
|
||||||
- In the results table, the `biased_indicators` column
|
- In the results table, the `biased_indicators` column
|
||||||
will falsely flag FreqAI target indicators defined in `set_freqai_targets()` as biased.
|
will falsely flag FreqAI target indicators defined in `set_freqai_targets()` as biased.
|
||||||
**These are not biased and can safely be ignored.**
|
**These are not biased and can safely be ignored.**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
markdown==3.8.2
|
markdown==3.8.2
|
||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
mkdocs-material==9.6.14
|
mkdocs-material==9.6.18
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.16
|
pymdown-extensions==10.16.1
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
mike==2.1.3
|
mike==2.1.3
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ The Order-type will be ignored if only one mode is available.
|
|||||||
| Binance | limit |
|
| Binance | limit |
|
||||||
| Binance Futures | market, limit |
|
| Binance Futures | market, limit |
|
||||||
| Bingx | market, limit |
|
| Bingx | market, limit |
|
||||||
|
| Bitget | market, limit |
|
||||||
| HTX | limit |
|
| HTX | limit |
|
||||||
| kraken | market, limit |
|
| kraken | market, limit |
|
||||||
| Gate | limit |
|
| Gate | limit |
|
||||||
|
|||||||
+37
-14
@@ -174,17 +174,27 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
## Enter Tag
|
## Enter Tag
|
||||||
|
|
||||||
When your strategy has multiple buy signals, you can name the signal that triggered.
|
When your strategy has multiple entry signals, you can name the signal that triggered.
|
||||||
Then you can access your buy signal on `custom_exit`
|
Then you can access your entry signal on `custom_exit`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe["enter_tag"] = ""
|
||||||
|
signal_rsi = (qtpylib.crossed_above(dataframe["rsi"], 35))
|
||||||
|
signal_bblower = (dataframe["bb_lowerband"] < dataframe["close"])
|
||||||
|
# Additional conditions
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['rsi'] < 35) &
|
signal_rsi
|
||||||
(dataframe['volume'] > 0)
|
| signal_bblower
|
||||||
),
|
# ... additional signals to enter a long position
|
||||||
['enter_long', 'enter_tag']] = (1, 'buy_signal_rsi')
|
)
|
||||||
|
& (dataframe["volume"] > 0)
|
||||||
|
, "enter_long"
|
||||||
|
] = 1
|
||||||
|
# Concatenate the tags so all signals are kept
|
||||||
|
dataframe.loc[signal_rsi, "enter_tag"] += "long_signal_rsi "
|
||||||
|
dataframe.loc[signal_bblower, "enter_tag"] += "long_signal_bblower "
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
@@ -192,14 +202,17 @@ def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_r
|
|||||||
current_profit: float, **kwargs):
|
current_profit: float, **kwargs):
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
last_candle = dataframe.iloc[-1].squeeze()
|
last_candle = dataframe.iloc[-1].squeeze()
|
||||||
if trade.enter_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80:
|
if "long_signal_rsi" in trade.enter_tag and last_candle["rsi"] > 80:
|
||||||
return 'sell_signal_rsi'
|
return "exit_signal_rsi"
|
||||||
|
if "long_signal_bblower" in trade.enter_tag and last_candle["high"] > last_candle["bb_upperband"]:
|
||||||
|
return "exit_signal_bblower"
|
||||||
|
# ...
|
||||||
return None
|
return None
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
`enter_tag` is limited to 100 characters, remaining data will be truncated.
|
`enter_tag` is limited to 255 characters, remaining data will be truncated.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
There is only one `enter_tag` column, which is used for both long and short trades.
|
There is only one `enter_tag` column, which is used for both long and short trades.
|
||||||
@@ -213,17 +226,27 @@ Similar to [Entry Tagging](#enter-tag), you can also specify an exit tag.
|
|||||||
|
|
||||||
``` python
|
``` python
|
||||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe["exit_tag"] = ""
|
||||||
|
rsi_exit_signal = (dataframe["rsi"] > 70)
|
||||||
|
ema_exit_signal = (dataframe["ema20"] < dataframe["ema50"])
|
||||||
|
# Additional conditions
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['rsi'] > 70) &
|
rsi_exit_signal
|
||||||
(dataframe['volume'] > 0)
|
| ema_exit_signal
|
||||||
),
|
# ... additional signals to exit a long position
|
||||||
['exit_long', 'exit_tag']] = (1, 'exit_rsi')
|
) &
|
||||||
|
(dataframe["volume"] > 0)
|
||||||
|
,
|
||||||
|
"exit_long"] = 1
|
||||||
|
# Concatenate the tags so all signals are kept
|
||||||
|
dataframe.loc[rsi_exit_signal, "exit_tag"] += "exit_signal_rsi "
|
||||||
|
dataframe.loc[rsi_exit_signal2, "exit_tag"] += "exit_signal_rsi "
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
The provided exit-tag is then used as sell-reason - and shown as such in backtest results.
|
The provided exit-tag is then used as exit-reason - and shown as such in backtest results.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
`exit_reason` is limited to 100 characters, remaining data will be truncated.
|
`exit_reason` is limited to 100 characters, remaining data will be truncated.
|
||||||
|
|||||||
@@ -47,3 +47,8 @@
|
|||||||
border-color: #afb8c1;
|
border-color: #afb8c1;
|
||||||
box-shadow: inset 0 1px 0 rgba(175, 184, 193, 0.2);
|
box-shadow: inset 0 1px 0 rgba(175, 184, 193, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-grid {
|
||||||
|
/* default is max-width: 61rem; */
|
||||||
|
max-width: 75rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
| `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade.
|
| `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade.
|
||||||
| **Metrics** |
|
| **Metrics** |
|
||||||
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
||||||
|
| `/profit_[long|short] [<n>]` | Display a summary of your profit/loss from close trades in one direction and some stats about your performance, over the last n days (all trades by default)
|
||||||
| `/performance` | Show performance of each finished trade grouped by pair
|
| `/performance` | Show performance of each finished trade grouped by pair
|
||||||
| `/balance` | Show bot managed balance per currency
|
| `/balance` | Show bot managed balance per currency
|
||||||
| `/balance full` | Show account balance per currency
|
| `/balance full` | Show account balance per currency
|
||||||
@@ -309,6 +310,8 @@ current max
|
|||||||
|
|
||||||
### /profit
|
### /profit
|
||||||
|
|
||||||
|
Also available as `/profit_long` and `/profit_short` to show profit for long or short trades only.
|
||||||
|
|
||||||
Return a summary of your profit/loss and performance.
|
Return a summary of your profit/loss and performance.
|
||||||
|
|
||||||
> **ROI:** Close trades
|
> **ROI:** Close trades
|
||||||
|
|||||||
@@ -42,7 +42,3 @@ freqtrade install-ui
|
|||||||
|
|
||||||
Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from updated dependencies, which fail to install (for example TA-lib).
|
Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from updated dependencies, which fail to install (for example TA-lib).
|
||||||
Please refer to the corresponding installation sections (common problems linked below)
|
Please refer to the corresponding installation sections (common problems linked below)
|
||||||
|
|
||||||
Common problems and their solutions:
|
|
||||||
|
|
||||||
* [ta-lib update on windows](windows_installation.md#install-ta-lib)
|
|
||||||
|
|||||||
+9
-52
@@ -117,9 +117,9 @@ Different payloads can be configured for different events. Not all fields are ne
|
|||||||
|
|
||||||
## Webhook Message types
|
## Webhook Message types
|
||||||
|
|
||||||
### Entry
|
### Entry / Entry fill
|
||||||
|
|
||||||
The fields in `webhook.entry` are filled when the bot executes a long/short. Parameters are filled using string.format.
|
The fields in `webhook.entry` and `webhook.entry_fill` are filled when the bot places a long/short Order to increase a position, or when that order fills respectively. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
* `trade_id`
|
* `trade_id`
|
||||||
@@ -162,31 +162,9 @@ Possible parameters are:
|
|||||||
* `current_rate`
|
* `current_rate`
|
||||||
* `enter_tag`
|
* `enter_tag`
|
||||||
|
|
||||||
### Entry fill
|
### Exit / Exit fill
|
||||||
|
|
||||||
The fields in `webhook.entry_fill` are filled when the bot filled a long/short order. Parameters are filled using string.format.
|
The fields in `webhook.exit` and `webhook.exit_fill` are filled when the bot places an exit order, or when that exit order fills respectively. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
|
||||||
|
|
||||||
* `trade_id`
|
|
||||||
* `exchange`
|
|
||||||
* `pair`
|
|
||||||
* `direction`
|
|
||||||
* `leverage`
|
|
||||||
* `open_rate`
|
|
||||||
* `amount`
|
|
||||||
* `open_date`
|
|
||||||
* `stake_amount`
|
|
||||||
* `stake_currency`
|
|
||||||
* `base_currency`
|
|
||||||
* `quote_currency`
|
|
||||||
* `fiat_currency`
|
|
||||||
* `order_type`
|
|
||||||
* `current_rate`
|
|
||||||
* `enter_tag`
|
|
||||||
|
|
||||||
### Exit
|
|
||||||
|
|
||||||
The fields in `webhook.exit` are filled when the bot exits a trade. Parameters are filled using string.format.
|
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
* `trade_id`
|
* `trade_id`
|
||||||
@@ -195,34 +173,9 @@ Possible parameters are:
|
|||||||
* `direction`
|
* `direction`
|
||||||
* `leverage`
|
* `leverage`
|
||||||
* `gain`
|
* `gain`
|
||||||
* `limit`
|
|
||||||
* `amount`
|
* `amount`
|
||||||
* `open_rate`
|
* `open_rate`
|
||||||
* `profit_amount`
|
|
||||||
* `profit_ratio`
|
|
||||||
* `stake_currency`
|
|
||||||
* `base_currency`
|
|
||||||
* `quote_currency`
|
|
||||||
* `fiat_currency`
|
|
||||||
* `exit_reason`
|
|
||||||
* `order_type`
|
|
||||||
* `open_date`
|
|
||||||
* `close_date`
|
|
||||||
|
|
||||||
### Exit fill
|
|
||||||
|
|
||||||
The fields in `webhook.exit_fill` are filled when the bot fills a exit order (closes a Trade). Parameters are filled using string.format.
|
|
||||||
Possible parameters are:
|
|
||||||
|
|
||||||
* `trade_id`
|
|
||||||
* `exchange`
|
|
||||||
* `pair`
|
|
||||||
* `direction`
|
|
||||||
* `leverage`
|
|
||||||
* `gain`
|
|
||||||
* `close_rate`
|
* `close_rate`
|
||||||
* `amount`
|
|
||||||
* `open_rate`
|
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
* `profit_amount`
|
* `profit_amount`
|
||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
@@ -230,10 +183,14 @@ Possible parameters are:
|
|||||||
* `base_currency`
|
* `base_currency`
|
||||||
* `quote_currency`
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
|
* `enter_tag`
|
||||||
* `exit_reason`
|
* `exit_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `open_date`
|
* `open_date`
|
||||||
* `close_date`
|
* `close_date`
|
||||||
|
* `sub_trade`
|
||||||
|
* `is_final_exit`
|
||||||
|
|
||||||
|
|
||||||
### Exit cancel
|
### Exit cancel
|
||||||
|
|
||||||
@@ -246,7 +203,7 @@ Possible parameters are:
|
|||||||
* `direction`
|
* `direction`
|
||||||
* `leverage`
|
* `leverage`
|
||||||
* `gain`
|
* `gain`
|
||||||
* `limit`
|
* `order_rate`
|
||||||
* `amount`
|
* `amount`
|
||||||
* `open_rate`
|
* `open_rate`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ We **strongly** recommend that Windows users use [Docker](docker_quickstart.md)
|
|||||||
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
||||||
Otherwise, please follow the instructions below.
|
Otherwise, please follow the instructions below.
|
||||||
|
|
||||||
All instructions assume that python 3.10+ is installed and available.
|
All instructions assume that python 3.11+ is installed and available.
|
||||||
|
|
||||||
## Clone the git repository
|
## Clone the git repository
|
||||||
|
|
||||||
@@ -38,30 +38,6 @@ cd freqtrade
|
|||||||
!!! Hint
|
!!! Hint
|
||||||
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
|
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
|
||||||
|
|
||||||
### Install ta-lib
|
|
||||||
|
|
||||||
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
|
|
||||||
|
|
||||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.10, 3.11, 3.12 and 3.13) and for 64bit Windows.
|
|
||||||
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
|
|
||||||
|
|
||||||
Other versions must be downloaded from the above link.
|
|
||||||
|
|
||||||
``` powershell
|
|
||||||
cd \path\freqtrade
|
|
||||||
python -m venv .venv
|
|
||||||
.venv\Scripts\activate.ps1
|
|
||||||
# optionally install ta-lib from wheel
|
|
||||||
# Eventually adjust the below filename to match the downloaded wheel
|
|
||||||
pip install --find-links build_helpers\ TA-Lib -U
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install -e .
|
|
||||||
freqtrade
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Note "Use Powershell"
|
|
||||||
The above installation script assumes you're using powershell on a 64bit windows.
|
|
||||||
Commands for the legacy CMD windows console may differ.
|
|
||||||
|
|
||||||
### Error during installation on Windows
|
### Error during installation on Windows
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Freqtrade bot"""
|
"""Freqtrade bot"""
|
||||||
|
|
||||||
__version__ = "2025.6"
|
__version__ = "2025.8"
|
||||||
|
|
||||||
if "dev" in __version__:
|
if "dev" in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
__main__.py for Freqtrade
|
__main__.py for Freqtrade
|
||||||
To launch Freqtrade as a module
|
To launch Freqtrade as a module
|
||||||
|
|
||||||
> python -m freqtrade (with Python >= 3.10)
|
> python -m freqtrade (with Python >= 3.11)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from freqtrade import main
|
from freqtrade import main
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def start_analysis_entries_exits(args: dict[str, Any]) -> None:
|
|||||||
from freqtrade.data.entryexitanalysis import process_entry_exit_reasons
|
from freqtrade.data.entryexitanalysis import process_entry_exit_reasons
|
||||||
|
|
||||||
# Initialize configuration
|
# Initialize configuration
|
||||||
config = setup_utils_configuration(args, RunMode.BACKTEST)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
logger.info("Starting freqtrade in analysis mode")
|
logger.info("Starting freqtrade in analysis mode")
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ ARGS_BACKTEST = [
|
|||||||
"strategy_list",
|
"strategy_list",
|
||||||
"export",
|
"export",
|
||||||
"exportfilename",
|
"exportfilename",
|
||||||
|
"exportdirectory",
|
||||||
"backtest_breakdown",
|
"backtest_breakdown",
|
||||||
"backtest_cache",
|
"backtest_cache",
|
||||||
"freqai_backtest_live_models",
|
"freqai_backtest_live_models",
|
||||||
@@ -94,9 +95,14 @@ ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column"]
|
|||||||
|
|
||||||
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column"]
|
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column"]
|
||||||
|
|
||||||
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"]
|
ARGS_BACKTEST_SHOW = [
|
||||||
|
"exportfilename",
|
||||||
|
"exportdirectory",
|
||||||
|
"backtest_show_pair_list",
|
||||||
|
"backtest_breakdown",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"]
|
||||||
|
|
||||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
||||||
|
|
||||||
@@ -233,6 +239,7 @@ ARGS_HYPEROPT_SHOW = [
|
|||||||
|
|
||||||
ARGS_ANALYZE_ENTRIES_EXITS = [
|
ARGS_ANALYZE_ENTRIES_EXITS = [
|
||||||
"exportfilename",
|
"exportfilename",
|
||||||
|
"exportdirectory",
|
||||||
"analysis_groups",
|
"analysis_groups",
|
||||||
"enter_reason_list",
|
"enter_reason_list",
|
||||||
"exit_reason_list",
|
"exit_reason_list",
|
||||||
@@ -258,24 +265,26 @@ ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs"
|
|||||||
|
|
||||||
# Command level configs - keep at the bottom of the above definitions
|
# Command level configs - keep at the bottom of the above definitions
|
||||||
NO_CONF_REQURIED = [
|
NO_CONF_REQURIED = [
|
||||||
|
"backtest-filter",
|
||||||
|
"backtesting-show",
|
||||||
"convert-data",
|
"convert-data",
|
||||||
"convert-trade-data",
|
"convert-trade-data",
|
||||||
"download-data",
|
"download-data",
|
||||||
"list-timeframes",
|
"hyperopt-list",
|
||||||
|
"hyperopt-show",
|
||||||
|
"list-data",
|
||||||
|
"list-freqaimodels",
|
||||||
|
"list-hyperoptloss",
|
||||||
"list-markets",
|
"list-markets",
|
||||||
"list-pairs",
|
"list-pairs",
|
||||||
"list-strategies",
|
"list-strategies",
|
||||||
"list-freqaimodels",
|
"list-timeframes",
|
||||||
"list-hyperoptloss",
|
|
||||||
"list-data",
|
|
||||||
"hyperopt-list",
|
|
||||||
"hyperopt-show",
|
|
||||||
"backtest-filter",
|
|
||||||
"plot-dataframe",
|
"plot-dataframe",
|
||||||
"plot-profit",
|
"plot-profit",
|
||||||
"show-trades",
|
"show-trades",
|
||||||
"trades-to-ohlcv",
|
"install-ui",
|
||||||
"strategy-updater",
|
"strategy-updater",
|
||||||
|
"trades-to-ohlcv",
|
||||||
]
|
]
|
||||||
|
|
||||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||||
@@ -311,8 +320,6 @@ class Arguments:
|
|||||||
# (see https://bugs.python.org/issue16399)
|
# (see https://bugs.python.org/issue16399)
|
||||||
# Allow no-config for certain commands (like downloading / plotting)
|
# Allow no-config for certain commands (like downloading / plotting)
|
||||||
if "config" in parsed_arg and parsed_arg.config is None:
|
if "config" in parsed_arg and parsed_arg.config is None:
|
||||||
conf_required = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
|
|
||||||
|
|
||||||
if "user_data_dir" in parsed_arg and parsed_arg.user_data_dir is not None:
|
if "user_data_dir" in parsed_arg and parsed_arg.user_data_dir is not None:
|
||||||
user_dir = parsed_arg.user_data_dir
|
user_dir = parsed_arg.user_data_dir
|
||||||
else:
|
else:
|
||||||
@@ -325,7 +332,9 @@ class Arguments:
|
|||||||
else:
|
else:
|
||||||
# Else use "config.json".
|
# Else use "config.json".
|
||||||
cfgfile = Path.cwd() / DEFAULT_CONFIG
|
cfgfile = Path.cwd() / DEFAULT_CONFIG
|
||||||
if cfgfile.is_file() or not conf_required:
|
conf_optional = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
|
||||||
|
if cfgfile.is_file() or not conf_optional:
|
||||||
|
# Only inject config if the file exists, or if the config is required
|
||||||
parsed_arg.config = [DEFAULT_CONFIG]
|
parsed_arg.config = [DEFAULT_CONFIG]
|
||||||
|
|
||||||
return parsed_arg
|
return parsed_arg
|
||||||
|
|||||||
@@ -199,22 +199,29 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`",
|
"(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`",
|
||||||
nargs="+",
|
nargs="+",
|
||||||
),
|
),
|
||||||
"export": Arg(
|
|
||||||
"--export",
|
|
||||||
help="Export backtest results (default: trades).",
|
|
||||||
choices=constants.EXPORT_OPTIONS,
|
|
||||||
),
|
|
||||||
"backtest_notes": Arg(
|
"backtest_notes": Arg(
|
||||||
"--notes",
|
"--notes",
|
||||||
help="Add notes to the backtest results.",
|
help="Add notes to the backtest results.",
|
||||||
metavar="TEXT",
|
metavar="TEXT",
|
||||||
),
|
),
|
||||||
|
"export": Arg(
|
||||||
|
"--export",
|
||||||
|
help="Export backtest results (default: trades).",
|
||||||
|
choices=constants.EXPORT_OPTIONS,
|
||||||
|
),
|
||||||
|
"exportdirectory": Arg(
|
||||||
|
"--backtest-directory",
|
||||||
|
"--export-directory",
|
||||||
|
help="Directory to use for backtest results. "
|
||||||
|
"Example: `--export-directory=user_data/backtest_results/`. ",
|
||||||
|
metavar="PATH",
|
||||||
|
),
|
||||||
"exportfilename": Arg(
|
"exportfilename": Arg(
|
||||||
"--export-filename",
|
|
||||||
"--backtest-filename",
|
"--backtest-filename",
|
||||||
|
"--export-filename",
|
||||||
help="Use this filename for backtest results."
|
help="Use this filename for backtest results."
|
||||||
"Requires `--export` to be set as well. "
|
"Example: `--backtest-filename=backtest_results_2020-09-27_16-20-48.json`. "
|
||||||
"Example: `--export-filename=user_data/backtest_results/backtest_today.json`",
|
"Assumes either `user_data/backtest_results/` or `--export-directory` as base directory.",
|
||||||
metavar="PATH",
|
metavar="PATH",
|
||||||
),
|
),
|
||||||
"disableparamexport": Arg(
|
"disableparamexport": Arg(
|
||||||
@@ -369,6 +376,11 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
help="Print all exchanges known to the ccxt library.",
|
help="Print all exchanges known to the ccxt library.",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
),
|
),
|
||||||
|
"dex_exchanges": Arg(
|
||||||
|
"--dex-exchanges",
|
||||||
|
help="Print only DEX exchanges.",
|
||||||
|
action="store_true",
|
||||||
|
),
|
||||||
# List pairs / markets
|
# List pairs / markets
|
||||||
"list_pairs_all": Arg(
|
"list_pairs_all": Arg(
|
||||||
"-a",
|
"-a",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Any
|
|||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config
|
||||||
from freqtrade.enums import CandleType, RunMode, TradingMode
|
from freqtrade.enums import CandleType, RunMode, TradingMode
|
||||||
from freqtrade.exceptions import ConfigurationError
|
from freqtrade.exceptions import ConfigurationError
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -134,7 +134,8 @@ def start_list_data(args: dict[str, Any]) -> None:
|
|||||||
config["datadir"], config.get("trading_mode", TradingMode.SPOT)
|
config["datadir"], config.get("trading_mode", TradingMode.SPOT)
|
||||||
)
|
)
|
||||||
if args["pairs"]:
|
if args["pairs"]:
|
||||||
paircombs = [comb for comb in paircombs if comb[0] in args["pairs"]]
|
pl = expand_pairlist(args["pairs"], [p[0] for p in paircombs], keep_invalid=True)
|
||||||
|
paircombs = [comb for comb in paircombs if comb[0] in pl]
|
||||||
title = f"Found {len(paircombs)} pair / timeframe combinations."
|
title = f"Found {len(paircombs)} pair / timeframe combinations."
|
||||||
if not config.get("show_timerange"):
|
if not config.get("show_timerange"):
|
||||||
groupedpair = defaultdict(list)
|
groupedpair = defaultdict(list)
|
||||||
@@ -197,7 +198,8 @@ def start_list_trades_data(args: dict[str, Any]) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if args["pairs"]:
|
if args["pairs"]:
|
||||||
paircombs = [comb for comb in paircombs if comb in args["pairs"]]
|
pl = expand_pairlist(args["pairs"], [p for p in paircombs], keep_invalid=True)
|
||||||
|
paircombs = [comb for comb in paircombs if comb in pl]
|
||||||
|
|
||||||
title = f"Found trades data for {len(paircombs)} {plural(len(paircombs), 'pair')}."
|
title = f"Found trades data for {len(paircombs)} {plural(len(paircombs), 'pair')}."
|
||||||
if not config.get("show_timerange"):
|
if not config.get("show_timerange"):
|
||||||
|
|||||||
@@ -46,7 +46,18 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
|||||||
table.add_column("Markets")
|
table.add_column("Markets")
|
||||||
table.add_column("Reason")
|
table.add_column("Reason")
|
||||||
|
|
||||||
|
trading_mode = args.get("trading_mode", None)
|
||||||
|
dex_only = args.get("dex_exchanges", False)
|
||||||
|
|
||||||
for exchange in available_exchanges:
|
for exchange in available_exchanges:
|
||||||
|
if trading_mode and not any(
|
||||||
|
a["trading_mode"] == trading_mode for a in exchange["trade_modes"]
|
||||||
|
):
|
||||||
|
# If trading_mode is specified, only show exchanges that support it
|
||||||
|
continue
|
||||||
|
if dex_only and not exchange.get("dex", False):
|
||||||
|
# If dex_only is specified, only show DEX exchanges
|
||||||
|
continue
|
||||||
name = Text(exchange["name"])
|
name = Text(exchange["name"])
|
||||||
if exchange["supported"]:
|
if exchange["supported"]:
|
||||||
name.append(" (Supported)", style="italic")
|
name.append(" (Supported)", style="italic")
|
||||||
@@ -135,6 +146,9 @@ def start_list_strategies(args: dict[str, Any]) -> None:
|
|||||||
strategy_objs = StrategyResolver.search_all_objects(
|
strategy_objs = StrategyResolver.search_all_objects(
|
||||||
config, not args["print_one_column"], config.get("recursive_strategy_search", False)
|
config, not args["print_one_column"], config.get("recursive_strategy_search", False)
|
||||||
)
|
)
|
||||||
|
if not strategy_objs:
|
||||||
|
logger.warning("No strategies found.")
|
||||||
|
return
|
||||||
# Sort alphabetically
|
# Sort alphabetically
|
||||||
strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
|
strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
|
||||||
for obj in strategy_objs:
|
for obj in strategy_objs:
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ def start_backtesting_show(args: dict[str, Any]) -> None:
|
|||||||
from freqtrade.data.btanalysis import load_backtest_stats
|
from freqtrade.data.btanalysis import load_backtest_stats
|
||||||
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
|
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
|
||||||
|
|
||||||
results = load_backtest_stats(config["exportfilename"])
|
results = load_backtest_stats(config["exportdirectory"], config["exportfilename"])
|
||||||
|
|
||||||
show_backtest_results(config, results)
|
show_backtest_results(config, results)
|
||||||
show_sorted_pairlist(config, results)
|
show_sorted_pairlist(config, results)
|
||||||
|
|||||||
@@ -157,6 +157,16 @@ CONF_SCHEMA = {
|
|||||||
"description": f"Offset for profit exit. {__IN_STRATEGY}",
|
"description": f"Offset for profit exit. {__IN_STRATEGY}",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
},
|
},
|
||||||
|
"recursive_strategy_search": {
|
||||||
|
"description": "Enable recursive strategy search.",
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"user_data_dir": {
|
||||||
|
"description": "Path to the user data directory.",
|
||||||
|
},
|
||||||
|
"datadir": {
|
||||||
|
"description": "Path to the data directory.",
|
||||||
|
},
|
||||||
"fee": {
|
"fee": {
|
||||||
"description": "Trading fee percentage. Can help to simulate slippage in backtesting",
|
"description": "Trading fee percentage. Can help to simulate slippage in backtesting",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
@@ -443,6 +453,7 @@ CONF_SCHEMA = {
|
|||||||
"pairlists": {
|
"pairlists": {
|
||||||
"description": "Configuration for pairlists.",
|
"description": "Configuration for pairlists.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1371,6 +1382,7 @@ SCHEMA_TRADE_REQUIRED = [
|
|||||||
"entry_pricing",
|
"entry_pricing",
|
||||||
"stoploss",
|
"stoploss",
|
||||||
"minimal_roi",
|
"minimal_roi",
|
||||||
|
"pairlists",
|
||||||
"internals",
|
"internals",
|
||||||
"dataformat_ohlcv",
|
"dataformat_ohlcv",
|
||||||
"dataformat_trades",
|
"dataformat_trades",
|
||||||
@@ -1380,6 +1392,7 @@ SCHEMA_BACKTEST_REQUIRED = [
|
|||||||
"exchange",
|
"exchange",
|
||||||
"stake_currency",
|
"stake_currency",
|
||||||
"stake_amount",
|
"stake_amount",
|
||||||
|
"pairlists",
|
||||||
"dry_run_wallet",
|
"dry_run_wallet",
|
||||||
"dataformat_ohlcv",
|
"dataformat_ohlcv",
|
||||||
"dataformat_trades",
|
"dataformat_trades",
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ def validate_config_schema(conf: dict[str, Any], preliminary: bool = False) -> d
|
|||||||
return conf
|
return conf
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.critical(f"Invalid configuration. Reason: {e}")
|
logger.critical(f"Invalid configuration. Reason: {e}")
|
||||||
raise ValidationError(best_match(Draft4Validator(conf_schema).iter_errors(conf)).message)
|
result = best_match(FreqtradeValidator(conf_schema).iter_errors(conf))
|
||||||
|
raise ConfigurationError(result.message)
|
||||||
|
|
||||||
|
|
||||||
def validate_config_consistency(conf: dict[str, Any], *, preliminary: bool = False) -> None:
|
def validate_config_consistency(conf: dict[str, Any], *, preliminary: bool = False) -> None:
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ from freqtrade.constants import Config
|
|||||||
from freqtrade.enums import (
|
from freqtrade.enums import (
|
||||||
NON_UTIL_MODES,
|
NON_UTIL_MODES,
|
||||||
TRADE_MODES,
|
TRADE_MODES,
|
||||||
CandleType,
|
|
||||||
MarginMode,
|
|
||||||
RunMode,
|
RunMode,
|
||||||
TradingMode,
|
|
||||||
)
|
)
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.loggers import setup_logging
|
from freqtrade.loggers import setup_logging
|
||||||
@@ -87,9 +84,6 @@ class Configuration:
|
|||||||
if "internals" not in config:
|
if "internals" not in config:
|
||||||
config["internals"] = {}
|
config["internals"] = {}
|
||||||
|
|
||||||
if "pairlists" not in config:
|
|
||||||
config["pairlists"] = []
|
|
||||||
|
|
||||||
# Keep a copy of the original configuration file
|
# Keep a copy of the original configuration file
|
||||||
config["original_config"] = deepcopy(config)
|
config["original_config"] = deepcopy(config)
|
||||||
|
|
||||||
@@ -215,13 +209,28 @@ class Configuration:
|
|||||||
config.update({"datadir": create_datadir(config, self.args.get("datadir"))})
|
config.update({"datadir": create_datadir(config, self.args.get("datadir"))})
|
||||||
logger.info("Using data directory: %s ...", config.get("datadir"))
|
logger.info("Using data directory: %s ...", config.get("datadir"))
|
||||||
|
|
||||||
|
self._args_to_config(
|
||||||
|
config, argname="exportdirectory", logstring="Using {} as backtest directory ..."
|
||||||
|
)
|
||||||
|
|
||||||
if self.args.get("exportfilename"):
|
if self.args.get("exportfilename"):
|
||||||
self._args_to_config(
|
self._args_to_config(
|
||||||
config, argname="exportfilename", logstring="Storing backtest results to {} ..."
|
config, argname="exportfilename", logstring="Storing backtest results to {} ..."
|
||||||
)
|
)
|
||||||
config["exportfilename"] = Path(config["exportfilename"])
|
config["exportfilename"] = Path(config["exportfilename"])
|
||||||
else:
|
if config.get("exportdirectory") and Path(config["exportdirectory"]).is_dir():
|
||||||
config["exportfilename"] = config["user_data_dir"] / "backtest_results"
|
logger.warning(
|
||||||
|
"DEPRECATED: Using `--export-filename` with directories is deprecated, "
|
||||||
|
"use `--backtest-directory` instead."
|
||||||
|
)
|
||||||
|
if config.get("exportdirectory") is None:
|
||||||
|
# Fallback - assign export-directory directly.
|
||||||
|
config["exportdirectory"] = config["exportfilename"]
|
||||||
|
if not config.get("exportdirectory"):
|
||||||
|
config["exportdirectory"] = config["user_data_dir"] / "backtest_results"
|
||||||
|
if not config.get("exportfilename"):
|
||||||
|
config["exportfilename"] = None
|
||||||
|
config["exportdirectory"] = Path(config["exportdirectory"])
|
||||||
|
|
||||||
if self.args.get("show_sensitive"):
|
if self.args.get("show_sensitive"):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -397,11 +406,6 @@ class Configuration:
|
|||||||
self._args_to_config(
|
self._args_to_config(
|
||||||
config, argname="trading_mode", logstring="Detected --trading-mode: {}"
|
config, argname="trading_mode", logstring="Detected --trading-mode: {}"
|
||||||
)
|
)
|
||||||
config["candle_type_def"] = CandleType.get_default(
|
|
||||||
config.get("trading_mode", "spot") or "spot"
|
|
||||||
)
|
|
||||||
config["trading_mode"] = TradingMode(config.get("trading_mode", "spot") or "spot")
|
|
||||||
config["margin_mode"] = MarginMode(config.get("margin_mode", "") or "")
|
|
||||||
self._args_to_config(
|
self._args_to_config(
|
||||||
config, argname="candle_types", logstring="Detected --candle-types: {}"
|
config, argname="candle_types", logstring="Detected --candle-types: {}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ This module contains the argument manager class
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
|
from typing import Self
|
||||||
from typing_extensions import Self
|
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.exceptions import ConfigurationError
|
from freqtrade.exceptions import ConfigurationError
|
||||||
@@ -151,9 +150,7 @@ class TimeRange:
|
|||||||
starts = rvals[index]
|
starts = rvals[index]
|
||||||
if stype[0] == "date" and len(starts) == 8:
|
if stype[0] == "date" and len(starts) == 8:
|
||||||
start = int(
|
start = int(
|
||||||
datetime.strptime(starts, "%Y%m%d")
|
datetime.strptime(starts, "%Y%m%d").replace(tzinfo=UTC).timestamp()
|
||||||
.replace(tzinfo=timezone.utc)
|
|
||||||
.timestamp()
|
|
||||||
)
|
)
|
||||||
elif len(starts) == 13:
|
elif len(starts) == 13:
|
||||||
start = int(starts) // 1000
|
start = int(starts) // 1000
|
||||||
@@ -164,9 +161,7 @@ class TimeRange:
|
|||||||
stops = rvals[index]
|
stops = rvals[index]
|
||||||
if stype[1] == "date" and len(stops) == 8:
|
if stype[1] == "date" and len(stops) == 8:
|
||||||
stop = int(
|
stop = int(
|
||||||
datetime.strptime(stops, "%Y%m%d")
|
datetime.strptime(stops, "%Y%m%d").replace(tzinfo=UTC).timestamp()
|
||||||
.replace(tzinfo=timezone.utc)
|
|
||||||
.timestamp()
|
|
||||||
)
|
)
|
||||||
elif len(stops) == 13:
|
elif len(stops) == 13:
|
||||||
stop = int(stops) // 1000
|
stop = int(stops) // 1000
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ from .bt_fileutils import (
|
|||||||
load_backtest_data,
|
load_backtest_data,
|
||||||
load_backtest_metadata,
|
load_backtest_metadata,
|
||||||
load_backtest_stats,
|
load_backtest_stats,
|
||||||
load_exit_signal_candles,
|
|
||||||
load_file_from_zip,
|
load_file_from_zip,
|
||||||
load_rejected_signals,
|
|
||||||
load_signal_candles,
|
|
||||||
load_trades,
|
load_trades,
|
||||||
load_trades_from_db,
|
load_trades_from_db,
|
||||||
trade_list_to_dataframe,
|
trade_list_to_dataframe,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Helpers when analyzing backtest data
|
|||||||
import logging
|
import logging
|
||||||
import zipfile
|
import zipfile
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from io import BytesIO, StringIO
|
from io import BytesIO, StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
@@ -155,33 +155,55 @@ def load_backtest_metadata(filename: Path | str) -> dict[str, Any]:
|
|||||||
raise OperationalException("Unexpected error while loading backtest metadata.") from e
|
raise OperationalException("Unexpected error while loading backtest metadata.") from e
|
||||||
|
|
||||||
|
|
||||||
def load_backtest_stats(filename: Path | str) -> BacktestResultType:
|
def _normalize_filename(file_or_directory: Path | str, filename: Path | str | None) -> Path:
|
||||||
|
"""
|
||||||
|
Normalize the filename by ensuring it is a Path object.
|
||||||
|
:param file_or_directory: The directory or file to normalize.
|
||||||
|
:param filename: The filename to normalize.
|
||||||
|
:return: A Path object representing the normalized filename.
|
||||||
|
"""
|
||||||
|
if isinstance(file_or_directory, str):
|
||||||
|
file_or_directory = Path(file_or_directory)
|
||||||
|
if file_or_directory.is_dir():
|
||||||
|
if not filename:
|
||||||
|
filename = get_latest_backtest_filename(file_or_directory)
|
||||||
|
if Path(filename).is_file():
|
||||||
|
fn = Path(filename)
|
||||||
|
else:
|
||||||
|
fn = file_or_directory / filename
|
||||||
|
else:
|
||||||
|
fn = file_or_directory
|
||||||
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
def load_backtest_stats(
|
||||||
|
file_or_directory: Path | str, filename: Path | str | None = None
|
||||||
|
) -> BacktestResultType:
|
||||||
"""
|
"""
|
||||||
Load backtest statistics file.
|
Load backtest statistics file.
|
||||||
:param filename: pathlib.Path object, or string pointing to the file.
|
:param file_or_directory: pathlib.Path object, or string pointing to the directory,
|
||||||
|
or absolute/relative path to the backtest results file.
|
||||||
|
:param filename: Optional filename to load from (if different from the main filename).
|
||||||
|
Only valid when loading from a directory.
|
||||||
:return: a dictionary containing the resulting file.
|
:return: a dictionary containing the resulting file.
|
||||||
"""
|
"""
|
||||||
if isinstance(filename, str):
|
fn = _normalize_filename(file_or_directory, filename)
|
||||||
filename = Path(filename)
|
|
||||||
if filename.is_dir():
|
|
||||||
filename = filename / get_latest_backtest_filename(filename)
|
|
||||||
if not filename.is_file():
|
|
||||||
raise ValueError(f"File {filename} does not exist.")
|
|
||||||
logger.info(f"Loading backtest result from {filename}")
|
|
||||||
|
|
||||||
if filename.suffix == ".zip":
|
if not fn.is_file():
|
||||||
|
raise ValueError(f"File or directory {fn} does not exist.")
|
||||||
|
logger.info(f"Loading backtest result from {fn}")
|
||||||
|
|
||||||
|
if fn.suffix == ".zip":
|
||||||
data = json_load(
|
data = json_load(
|
||||||
StringIO(
|
StringIO(load_file_from_zip(fn, fn.with_suffix(".json").name).decode("utf-8"))
|
||||||
load_file_from_zip(filename, filename.with_suffix(".json").name).decode("utf-8")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
with filename.open() as file:
|
with fn.open() as file:
|
||||||
data = json_load(file)
|
data = json_load(file)
|
||||||
|
|
||||||
# Legacy list format does not contain metadata.
|
# Legacy list format does not contain metadata.
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
data["metadata"] = load_backtest_metadata(filename)
|
data["metadata"] = load_backtest_metadata(fn)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -324,7 +346,7 @@ def find_existing_backtest_stats(
|
|||||||
|
|
||||||
if min_backtest_date is not None:
|
if min_backtest_date is not None:
|
||||||
backtest_date = strategy_metadata["backtest_start_time"]
|
backtest_date = strategy_metadata["backtest_start_time"]
|
||||||
backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc)
|
backtest_date = datetime.fromtimestamp(backtest_date, tz=UTC)
|
||||||
if backtest_date < min_backtest_date:
|
if backtest_date < min_backtest_date:
|
||||||
# Do not use a cached result for this strategy as first result is too old.
|
# Do not use a cached result for this strategy as first result is too old.
|
||||||
del run_ids[strategy_name]
|
del run_ids[strategy_name]
|
||||||
@@ -362,16 +384,21 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
|
|||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
def load_backtest_data(filename: Path | str, strategy: str | None = None) -> pd.DataFrame:
|
def load_backtest_data(
|
||||||
|
file_or_directory: Path | str, strategy: str | None = None, filename: Path | str | None = None
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Load backtest data file.
|
Load backtest data file, returns a dataframe with the individual trades.
|
||||||
:param filename: pathlib.Path object, or string pointing to a file or directory
|
:param file_or_directory: pathlib.Path object, or string pointing to the directory,
|
||||||
|
or absolute/relative path to the backtest results file.
|
||||||
:param strategy: Strategy to load - mainly relevant for multi-strategy backtests
|
:param strategy: Strategy to load - mainly relevant for multi-strategy backtests
|
||||||
Can also serve as protection to load the correct result.
|
Can also serve as protection to load the correct result.
|
||||||
|
:param filename: Optional filename to load from (if different from the main filename).
|
||||||
|
Only valid when loading from a directory.
|
||||||
:return: a dataframe with the analysis results
|
:return: a dataframe with the analysis results
|
||||||
:raise: ValueError if loading goes wrong.
|
:raise: ValueError if loading goes wrong.
|
||||||
"""
|
"""
|
||||||
data = load_backtest_stats(filename)
|
data = load_backtest_stats(file_or_directory, filename)
|
||||||
if not isinstance(data, list):
|
if not isinstance(data, list):
|
||||||
# new, nested format
|
# new, nested format
|
||||||
if "strategy" not in data:
|
if "strategy" not in data:
|
||||||
@@ -430,20 +457,23 @@ def load_file_from_zip(zip_path: Path, filename: str) -> bytes:
|
|||||||
raise ValueError(f"Bad zip file: {zip_path}.") from None
|
raise ValueError(f"Bad zip file: {zip_path}.") from None
|
||||||
|
|
||||||
|
|
||||||
def load_backtest_analysis_data(backtest_dir: Path, name: str):
|
def load_backtest_analysis_data(
|
||||||
|
file_or_directory: Path,
|
||||||
|
name: Literal["signals", "rejected", "exited"],
|
||||||
|
filename: Path | str | None = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Load backtest analysis data either from a pickle file or from within a zip file
|
Load backtest analysis data either from a pickle file or from within a zip file
|
||||||
:param backtest_dir: Directory containing backtest results
|
:param file_or_directory: pathlib.Path object, or string pointing to the directory,
|
||||||
|
or absolute/relative path to the backtest results file.
|
||||||
:param name: Name of the analysis data to load (signals, rejected, exited)
|
:param name: Name of the analysis data to load (signals, rejected, exited)
|
||||||
|
:param filename: Optional filename to load from (if different from the main filename).
|
||||||
|
Only valid when loading from a directory.
|
||||||
:return: Analysis data
|
:return: Analysis data
|
||||||
"""
|
"""
|
||||||
import joblib
|
import joblib
|
||||||
|
|
||||||
if backtest_dir.is_dir():
|
zip_path = _normalize_filename(file_or_directory, filename)
|
||||||
lbf = Path(get_latest_backtest_filename(backtest_dir))
|
|
||||||
zip_path = backtest_dir / lbf
|
|
||||||
else:
|
|
||||||
zip_path = backtest_dir
|
|
||||||
|
|
||||||
if zip_path.suffix == ".zip":
|
if zip_path.suffix == ".zip":
|
||||||
# Load from zip file
|
# Load from zip file
|
||||||
@@ -458,10 +488,10 @@ def load_backtest_analysis_data(backtest_dir: Path, name: str):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# Load from separate pickle file
|
# Load from separate pickle file
|
||||||
if backtest_dir.is_dir():
|
if file_or_directory.is_dir():
|
||||||
scpf = Path(backtest_dir, f"{zip_path.stem}_{name}.pkl")
|
scpf = Path(file_or_directory, f"{zip_path.stem}_{name}.pkl")
|
||||||
else:
|
else:
|
||||||
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl")
|
scpf = Path(file_or_directory.parent / f"{file_or_directory.stem}_{name}.pkl")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with scpf.open("rb") as scp:
|
with scpf.open("rb") as scp:
|
||||||
@@ -473,27 +503,6 @@ def load_backtest_analysis_data(backtest_dir: Path, name: str):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def load_rejected_signals(backtest_dir: Path):
|
|
||||||
"""
|
|
||||||
Load rejected signals from backtest directory
|
|
||||||
"""
|
|
||||||
return load_backtest_analysis_data(backtest_dir, "rejected")
|
|
||||||
|
|
||||||
|
|
||||||
def load_signal_candles(backtest_dir: Path):
|
|
||||||
"""
|
|
||||||
Load signal candles from backtest directory
|
|
||||||
"""
|
|
||||||
return load_backtest_analysis_data(backtest_dir, "signals")
|
|
||||||
|
|
||||||
|
|
||||||
def load_exit_signal_candles(backtest_dir: Path) -> dict[str, dict[str, pd.DataFrame]]:
|
|
||||||
"""
|
|
||||||
Load exit signal candles from backtest directory
|
|
||||||
"""
|
|
||||||
return load_backtest_analysis_data(backtest_dir, "exited")
|
|
||||||
|
|
||||||
|
|
||||||
def trade_list_to_dataframe(trades: list[Trade] | list[LocalTrade]) -> pd.DataFrame:
|
def trade_list_to_dataframe(trades: list[Trade] | list[LocalTrade]) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Convert list of Trade objects to pandas Dataframe
|
Convert list of Trade objects to pandas Dataframe
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ def get_tick_size_over_time(candles: DataFrame) -> Series:
|
|||||||
# count the number of significant digits for the open and close prices
|
# count the number of significant digits for the open and close prices
|
||||||
for col in ["open", "high", "low", "close"]:
|
for col in ["open", "high", "low", "close"]:
|
||||||
candles[f"{col}_count"] = (
|
candles[f"{col}_count"] = (
|
||||||
candles[col].round(14).astype(str).str.extract(r"\.(\d*[1-9])")[0].str.len()
|
candles[col].round(14).apply("{:.15f}".format).str.extract(r"\.(\d*[1-9])")[0].str.len()
|
||||||
)
|
)
|
||||||
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
|
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
|
||||||
axis=1
|
axis=1
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Common Interface for bot and strategy to access data.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
|
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
|
||||||
@@ -98,7 +98,7 @@ class DataProvider:
|
|||||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
"""
|
"""
|
||||||
pair_key = (pair, timeframe, candle_type)
|
pair_key = (pair, timeframe, candle_type)
|
||||||
self.__cached_pairs[pair_key] = (dataframe, datetime.now(timezone.utc))
|
self.__cached_pairs[pair_key] = (dataframe, datetime.now(UTC))
|
||||||
|
|
||||||
# For multiple producers we will want to merge the pairlists instead of overwriting
|
# For multiple producers we will want to merge the pairlists instead of overwriting
|
||||||
def _set_producer_pairs(self, pairlist: list[str], producer_name: str = "default"):
|
def _set_producer_pairs(self, pairlist: list[str], producer_name: str = "default"):
|
||||||
@@ -131,7 +131,7 @@ class DataProvider:
|
|||||||
"data": {
|
"data": {
|
||||||
"key": pair_key,
|
"key": pair_key,
|
||||||
"df": dataframe.tail(1),
|
"df": dataframe.tail(1),
|
||||||
"la": datetime.now(timezone.utc),
|
"la": datetime.now(UTC),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
self.__rpc.send_msg(msg)
|
self.__rpc.send_msg(msg)
|
||||||
@@ -164,7 +164,7 @@ class DataProvider:
|
|||||||
if producer_name not in self.__producer_pairs_df:
|
if producer_name not in self.__producer_pairs_df:
|
||||||
self.__producer_pairs_df[producer_name] = {}
|
self.__producer_pairs_df[producer_name] = {}
|
||||||
|
|
||||||
_last_analyzed = datetime.now(timezone.utc) if not last_analyzed else last_analyzed
|
_last_analyzed = datetime.now(UTC) if not last_analyzed else last_analyzed
|
||||||
|
|
||||||
self.__producer_pairs_df[producer_name][pair_key] = (dataframe, _last_analyzed)
|
self.__producer_pairs_df[producer_name][pair_key] = (dataframe, _last_analyzed)
|
||||||
logger.debug(f"External DataFrame for {pair_key} from {producer_name} added.")
|
logger.debug(f"External DataFrame for {pair_key} from {producer_name} added.")
|
||||||
@@ -275,12 +275,12 @@ class DataProvider:
|
|||||||
# If we have no data from this Producer yet
|
# If we have no data from this Producer yet
|
||||||
if producer_name not in self.__producer_pairs_df:
|
if producer_name not in self.__producer_pairs_df:
|
||||||
# We don't have this data yet, return empty DataFrame and datetime (01-01-1970)
|
# We don't have this data yet, return empty DataFrame and datetime (01-01-1970)
|
||||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
|
||||||
|
|
||||||
# If we do have data from that Producer, but no data on this pair_key
|
# If we do have data from that Producer, but no data on this pair_key
|
||||||
if pair_key not in self.__producer_pairs_df[producer_name]:
|
if pair_key not in self.__producer_pairs_df[producer_name]:
|
||||||
# We don't have this data yet, return empty DataFrame and datetime (01-01-1970)
|
# We don't have this data yet, return empty DataFrame and datetime (01-01-1970)
|
||||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
|
||||||
|
|
||||||
# We have it, return this data
|
# We have it, return this data
|
||||||
df, la = self.__producer_pairs_df[producer_name][pair_key]
|
df, la = self.__producer_pairs_df[producer_name][pair_key]
|
||||||
@@ -396,10 +396,10 @@ class DataProvider:
|
|||||||
if (max_index := self.__slice_index.get(pair)) is not None:
|
if (max_index := self.__slice_index.get(pair)) is not None:
|
||||||
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES) : max_index]
|
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES) : max_index]
|
||||||
else:
|
else:
|
||||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
|
||||||
return df, date
|
return df, date
|
||||||
else:
|
else:
|
||||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def runmode(self) -> RunMode:
|
def runmode(self) -> RunMode:
|
||||||
|
|||||||
@@ -7,11 +7,9 @@ from freqtrade.configuration import TimeRange
|
|||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.data.btanalysis import (
|
from freqtrade.data.btanalysis import (
|
||||||
BT_DATA_COLUMNS,
|
BT_DATA_COLUMNS,
|
||||||
|
load_backtest_analysis_data,
|
||||||
load_backtest_data,
|
load_backtest_data,
|
||||||
load_backtest_stats,
|
load_backtest_stats,
|
||||||
load_exit_signal_candles,
|
|
||||||
load_rejected_signals,
|
|
||||||
load_signal_candles,
|
|
||||||
)
|
)
|
||||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
from freqtrade.exceptions import ConfigurationError, OperationalException
|
||||||
from freqtrade.util import print_df_rich_table
|
from freqtrade.util import print_df_rich_table
|
||||||
@@ -332,7 +330,7 @@ def process_entry_exit_reasons(config: Config):
|
|||||||
do_rejected = config.get("analysis_rejected", False)
|
do_rejected = config.get("analysis_rejected", False)
|
||||||
to_csv = config.get("analysis_to_csv", False)
|
to_csv = config.get("analysis_to_csv", False)
|
||||||
csv_path = Path(
|
csv_path = Path(
|
||||||
config.get("analysis_csv_path", config["exportfilename"]), # type: ignore[arg-type]
|
config.get("analysis_csv_path", config["exportdirectory"]), # type: ignore[arg-type]
|
||||||
)
|
)
|
||||||
|
|
||||||
if entry_only is True and exit_only is True:
|
if entry_only is True and exit_only is True:
|
||||||
@@ -346,20 +344,30 @@ def process_entry_exit_reasons(config: Config):
|
|||||||
None if config.get("timerange") is None else str(config.get("timerange"))
|
None if config.get("timerange") is None else str(config.get("timerange"))
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
backtest_stats = load_backtest_stats(config["exportfilename"])
|
backtest_stats = load_backtest_stats(
|
||||||
|
config["exportdirectory"], config["exportfilename"]
|
||||||
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise ConfigurationError(e) from e
|
raise ConfigurationError(e) from e
|
||||||
|
|
||||||
for strategy_name, results in backtest_stats["strategy"].items():
|
for strategy_name, results in backtest_stats["strategy"].items():
|
||||||
trades = load_backtest_data(config["exportfilename"], strategy_name)
|
trades = load_backtest_data(
|
||||||
|
config["exportdirectory"], strategy_name, config["exportfilename"]
|
||||||
|
)
|
||||||
|
|
||||||
if trades is not None and not trades.empty:
|
if trades is not None and not trades.empty:
|
||||||
signal_candles = load_signal_candles(config["exportfilename"])
|
signal_candles = load_backtest_analysis_data(
|
||||||
exit_signals = load_exit_signal_candles(config["exportfilename"])
|
config["exportdirectory"], "signals", config["exportfilename"]
|
||||||
|
)
|
||||||
|
exit_signals = load_backtest_analysis_data(
|
||||||
|
config["exportdirectory"], "exited", config["exportfilename"]
|
||||||
|
)
|
||||||
|
|
||||||
rej_df = None
|
rej_df = None
|
||||||
if do_rejected:
|
if do_rejected:
|
||||||
rejected_signals_dict = load_rejected_signals(config["exportfilename"])
|
rejected_signals_dict = load_backtest_analysis_data(
|
||||||
|
config["exportdirectory"], "rejected", config["exportfilename"]
|
||||||
|
)
|
||||||
rej_df = prepare_results(
|
rej_df = prepare_results(
|
||||||
rejected_signals_dict,
|
rejected_signals_dict,
|
||||||
strategy_name,
|
strategy_name,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
@@ -118,8 +118,8 @@ class IDataHandler(ABC):
|
|||||||
df = self._ohlcv_load(pair, timeframe, None, candle_type)
|
df = self._ohlcv_load(pair, timeframe, None, candle_type)
|
||||||
if df.empty:
|
if df.empty:
|
||||||
return (
|
return (
|
||||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
datetime.fromtimestamp(0, tz=UTC),
|
||||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
datetime.fromtimestamp(0, tz=UTC),
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
return df.iloc[0]["date"].to_pydatetime(), df.iloc[-1]["date"].to_pydatetime(), len(df)
|
return df.iloc[0]["date"].to_pydatetime(), df.iloc[-1]["date"].to_pydatetime(), len(df)
|
||||||
@@ -201,8 +201,8 @@ class IDataHandler(ABC):
|
|||||||
df = self._trades_load(pair, trading_mode)
|
df = self._trades_load(pair, trading_mode)
|
||||||
if df.empty:
|
if df.empty:
|
||||||
return (
|
return (
|
||||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
datetime.fromtimestamp(0, tz=UTC),
|
||||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
datetime.fromtimestamp(0, tz=UTC),
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ def load_data(
|
|||||||
"""
|
"""
|
||||||
result: dict[str, DataFrame] = {}
|
result: dict[str, DataFrame] = {}
|
||||||
if startup_candles > 0 and timerange:
|
if startup_candles > 0 and timerange:
|
||||||
logger.info(f"Using indicator startup period: {startup_candles} ...")
|
logger.debug(f"Using indicator startup period: {startup_candles} ...")
|
||||||
|
|
||||||
data_handler = get_datahandler(datadir, data_format)
|
data_handler = get_datahandler(datadir, data_format)
|
||||||
|
|
||||||
|
|||||||
@@ -174,12 +174,18 @@ def calculate_underwater(
|
|||||||
|
|
||||||
@dataclass()
|
@dataclass()
|
||||||
class DrawDownResult:
|
class DrawDownResult:
|
||||||
|
# Max drawdown fields
|
||||||
drawdown_abs: float = 0.0
|
drawdown_abs: float = 0.0
|
||||||
high_date: pd.Timestamp = None
|
high_date: pd.Timestamp = None
|
||||||
low_date: pd.Timestamp = None
|
low_date: pd.Timestamp = None
|
||||||
high_value: float = 0.0
|
high_value: float = 0.0
|
||||||
low_value: float = 0.0
|
low_value: float = 0.0
|
||||||
relative_account_drawdown: float = 0.0
|
relative_account_drawdown: float = 0.0
|
||||||
|
# Current drawdown fields
|
||||||
|
current_high_date: pd.Timestamp = None
|
||||||
|
current_high_value: float = 0.0
|
||||||
|
current_drawdown_abs: float = 0.0
|
||||||
|
current_relative_account_drawdown: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
def calculate_max_drawdown(
|
def calculate_max_drawdown(
|
||||||
@@ -191,29 +197,31 @@ def calculate_max_drawdown(
|
|||||||
relative: bool = False,
|
relative: bool = False,
|
||||||
) -> DrawDownResult:
|
) -> DrawDownResult:
|
||||||
"""
|
"""
|
||||||
Calculate max drawdown and the corresponding close dates
|
Calculate max drawdown and current drawdown with corresponding dates
|
||||||
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||||
:param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
|
:param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
|
||||||
:param value_col: Column in DataFrame to use for values (defaults to 'profit_abs')
|
:param value_col: Column in DataFrame to use for values (defaults to 'profit_abs')
|
||||||
:param starting_balance: Portfolio starting balance - properly calculate relative drawdown.
|
:param starting_balance: Portfolio starting balance - properly calculate relative drawdown.
|
||||||
|
:param relative: If True, use relative drawdown for max calculation instead of absolute
|
||||||
:return: DrawDownResult object
|
:return: DrawDownResult object
|
||||||
with absolute max drawdown, high and low time and high and low value,
|
with absolute max drawdown, high and low time and high and low value,
|
||||||
and the relative account drawdown
|
relative account drawdown, and current drawdown information.
|
||||||
:raise: ValueError if trade-dataframe was found empty.
|
:raise: ValueError if trade-dataframe was found empty.
|
||||||
"""
|
"""
|
||||||
if len(trades) == 0:
|
if len(trades) == 0:
|
||||||
raise ValueError("Trade dataframe empty.")
|
raise ValueError("Trade dataframe empty.")
|
||||||
|
|
||||||
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
||||||
max_drawdown_df = _calc_drawdown_series(
|
max_drawdown_df = _calc_drawdown_series(
|
||||||
profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance
|
profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Calculate maximum drawdown
|
||||||
idxmin = (
|
idxmin = (
|
||||||
max_drawdown_df["drawdown_relative"].idxmax()
|
max_drawdown_df["drawdown_relative"].idxmax()
|
||||||
if relative
|
if relative
|
||||||
else max_drawdown_df["drawdown"].idxmin()
|
else max_drawdown_df["drawdown"].idxmin()
|
||||||
)
|
)
|
||||||
|
|
||||||
high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax()
|
high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax()
|
||||||
high_date = profit_results.loc[high_idx, date_col]
|
high_date = profit_results.loc[high_idx, date_col]
|
||||||
low_date = profit_results.loc[idxmin, date_col]
|
low_date = profit_results.loc[idxmin, date_col]
|
||||||
@@ -221,13 +229,27 @@ def calculate_max_drawdown(
|
|||||||
low_val = max_drawdown_df.loc[idxmin, "cumulative"]
|
low_val = max_drawdown_df.loc[idxmin, "cumulative"]
|
||||||
max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"]
|
max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"]
|
||||||
|
|
||||||
|
# Calculate current drawdown
|
||||||
|
current_high_idx = max_drawdown_df["high_value"].iloc[:-1].idxmax()
|
||||||
|
current_high_date = profit_results.loc[current_high_idx, date_col]
|
||||||
|
current_high_value = max_drawdown_df.iloc[-1]["high_value"]
|
||||||
|
current_cumulative = max_drawdown_df.iloc[-1]["cumulative"]
|
||||||
|
current_drawdown_abs = current_high_value - current_cumulative
|
||||||
|
current_drawdown_relative = max_drawdown_df.iloc[-1]["drawdown_relative"]
|
||||||
|
|
||||||
return DrawDownResult(
|
return DrawDownResult(
|
||||||
|
# Max drawdown
|
||||||
drawdown_abs=abs(max_drawdown_df.loc[idxmin, "drawdown"]),
|
drawdown_abs=abs(max_drawdown_df.loc[idxmin, "drawdown"]),
|
||||||
high_date=high_date,
|
high_date=high_date,
|
||||||
low_date=low_date,
|
low_date=low_date,
|
||||||
high_value=high_val,
|
high_value=high_val,
|
||||||
low_value=low_val,
|
low_value=low_val,
|
||||||
relative_account_drawdown=max_drawdown_rel,
|
relative_account_drawdown=max_drawdown_rel,
|
||||||
|
# Current drawdown
|
||||||
|
current_high_date=current_high_date,
|
||||||
|
current_high_value=current_high_value,
|
||||||
|
current_drawdown_abs=current_drawdown_abs,
|
||||||
|
current_relative_account_drawdown=current_drawdown_relative,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from freqtrade.exchange.exchange import Exchange
|
|||||||
# isort: on
|
# isort: on
|
||||||
from freqtrade.exchange.binance import Binance
|
from freqtrade.exchange.binance import Binance
|
||||||
from freqtrade.exchange.bingx import Bingx
|
from freqtrade.exchange.bingx import Bingx
|
||||||
|
from freqtrade.exchange.bitget import Bitget
|
||||||
from freqtrade.exchange.bitmart import Bitmart
|
from freqtrade.exchange.bitmart import Bitmart
|
||||||
from freqtrade.exchange.bitpanda import Bitpanda
|
from freqtrade.exchange.bitpanda import Bitpanda
|
||||||
from freqtrade.exchange.bitvavo import Bitvavo
|
from freqtrade.exchange.bitvavo import Bitvavo
|
||||||
@@ -43,4 +44,6 @@ from freqtrade.exchange.idex import Idex
|
|||||||
from freqtrade.exchange.kraken import Kraken
|
from freqtrade.exchange.kraken import Kraken
|
||||||
from freqtrade.exchange.kucoin import Kucoin
|
from freqtrade.exchange.kucoin import Kucoin
|
||||||
from freqtrade.exchange.lbank import Lbank
|
from freqtrade.exchange.lbank import Lbank
|
||||||
from freqtrade.exchange.okx import Okx
|
from freqtrade.exchange.luno import Luno
|
||||||
|
from freqtrade.exchange.modetrade import Modetrade
|
||||||
|
from freqtrade.exchange.okx import MyOkx, Okx
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Binance exchange subclass"""
|
"""Binance exchange subclass"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
@@ -45,7 +45,6 @@ class Binance(Exchange):
|
|||||||
"funding_fee_candle_limit": 1000,
|
"funding_fee_candle_limit": 1000,
|
||||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||||
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
||||||
"order_time_in_force": ["GTC", "FOK", "IOC"],
|
|
||||||
"tickers_have_price": False,
|
"tickers_have_price": False,
|
||||||
"floor_leverage": True,
|
"floor_leverage": True,
|
||||||
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
|
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
|
||||||
@@ -63,7 +62,7 @@ class Binance(Exchange):
|
|||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
# TradingMode.SPOT always supported and not required in this list
|
(TradingMode.SPOT, MarginMode.NONE),
|
||||||
# (TradingMode.MARGIN, MarginMode.CROSS),
|
# (TradingMode.MARGIN, MarginMode.CROSS),
|
||||||
(TradingMode.FUTURES, MarginMode.CROSS),
|
(TradingMode.FUTURES, MarginMode.CROSS),
|
||||||
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||||
@@ -160,7 +159,7 @@ class Binance(Exchange):
|
|||||||
since_ms = x[3][0][0]
|
since_ms = x[3][0][0]
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Candle-data for {pair} available starting with "
|
f"Candle-data for {pair} available starting with "
|
||||||
f"{datetime.fromtimestamp(since_ms // 1000, tz=timezone.utc).isoformat()}."
|
f"{datetime.fromtimestamp(since_ms // 1000, tz=UTC).isoformat()}."
|
||||||
)
|
)
|
||||||
if until_ms and since_ms >= until_ms:
|
if until_ms and since_ms >= until_ms:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -399,7 +398,7 @@ class Binance(Exchange):
|
|||||||
trades = await self._api_async.fetch_trades(
|
trades = await self._api_async.fetch_trades(
|
||||||
pair,
|
pair,
|
||||||
params={
|
params={
|
||||||
self._trades_pagination_arg: "0",
|
self._ft_has["trades_pagination_arg"]: "0",
|
||||||
},
|
},
|
||||||
limit=5,
|
limit=5,
|
||||||
)
|
)
|
||||||
|
|||||||
+18339
-13329
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
|
||||||
|
from freqtrade.enums import CandleType
|
||||||
|
from freqtrade.exceptions import (
|
||||||
|
DDosProtection,
|
||||||
|
OperationalException,
|
||||||
|
RetryableOrderError,
|
||||||
|
TemporaryError,
|
||||||
|
)
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
|
||||||
|
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||||
|
from freqtrade.util.datetime_helpers import dt_now, dt_ts
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Bitget(Exchange):
|
||||||
|
"""
|
||||||
|
Bitget 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 = {
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stop_price_param": "stopPrice",
|
||||||
|
"stop_price_prop": "stopPrice",
|
||||||
|
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||||
|
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
|
||||||
|
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||||
|
}
|
||||||
|
_ft_has_futures: FtHas = {
|
||||||
|
"mark_ohlcv_timeframe": "4h",
|
||||||
|
}
|
||||||
|
|
||||||
|
def ohlcv_candle_limit(
|
||||||
|
self, timeframe: str, candle_type: CandleType, since_ms: int | None = None
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Exchange ohlcv candle limit
|
||||||
|
bitget has the following behaviour:
|
||||||
|
* 1000 candles for up-to-date data
|
||||||
|
* 200 candles for historic data (prior to a certain date)
|
||||||
|
:param timeframe: Timeframe to check
|
||||||
|
:param candle_type: Candle-type
|
||||||
|
:param since_ms: Starting timestamp
|
||||||
|
:return: Candle limit as integer
|
||||||
|
"""
|
||||||
|
timeframe_map = self._api.options["fetchOHLCV"]["maxRecentDaysPerTimeframe"]
|
||||||
|
days = timeframe_map.get(timeframe, 30)
|
||||||
|
|
||||||
|
if candle_type in (CandleType.FUTURES, CandleType.SPOT, CandleType.MARK) and (
|
||||||
|
not since_ms or dt_ts(dt_now() - timedelta(days=days)) < since_ms
|
||||||
|
):
|
||||||
|
return 1000
|
||||||
|
|
||||||
|
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
|
||||||
|
|
||||||
|
def _convert_stop_order(self, pair: str, order_id: str, order: CcxtOrder) -> CcxtOrder:
|
||||||
|
if order.get("status", "open") == "closed":
|
||||||
|
# Use orderID as cliendOrderId filter to fetch the regular followup order.
|
||||||
|
# Could be done with "fetch_order" - but clientOid as filter doesn't seem to work
|
||||||
|
# https://www.bitget.com/api-doc/spot/trade/Get-Order-Info
|
||||||
|
|
||||||
|
for method in (
|
||||||
|
self._api.fetch_canceled_and_closed_orders,
|
||||||
|
self._api.fetch_open_orders,
|
||||||
|
):
|
||||||
|
orders = method(pair)
|
||||||
|
orders_f = [order for order in orders if order["clientOrderId"] == order_id]
|
||||||
|
if orders_f:
|
||||||
|
order_reg = orders_f[0]
|
||||||
|
self._log_exchange_response("fetch_stoploss_order1", order_reg)
|
||||||
|
order_reg["id_stop"] = order_reg["id"]
|
||||||
|
order_reg["id"] = order_id
|
||||||
|
order_reg["type"] = "stoploss"
|
||||||
|
order_reg["status_stop"] = "triggered"
|
||||||
|
return order_reg
|
||||||
|
order = self._order_contracts_to_amount(order)
|
||||||
|
order["type"] = "stoploss"
|
||||||
|
return order
|
||||||
|
|
||||||
|
def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> CcxtOrder:
|
||||||
|
params2 = {
|
||||||
|
"stop": True,
|
||||||
|
}
|
||||||
|
for method in (
|
||||||
|
self._api.fetch_open_orders,
|
||||||
|
self._api.fetch_canceled_and_closed_orders,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
orders = method(pair, params=params2)
|
||||||
|
orders_f = [order for order in orders if order["id"] == order_id]
|
||||||
|
if orders_f:
|
||||||
|
order = orders_f[0]
|
||||||
|
self._log_exchange_response("get_stop_order_fallback", order)
|
||||||
|
return self._convert_stop_order(pair, order_id, order)
|
||||||
|
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
|
||||||
|
pass
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
|
||||||
|
) from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
|
||||||
|
|
||||||
|
@retrier(retries=API_RETRY_COUNT)
|
||||||
|
def fetch_stoploss_order(
|
||||||
|
self, order_id: str, pair: str, params: dict | None = None
|
||||||
|
) -> CcxtOrder:
|
||||||
|
if self._config["dry_run"]:
|
||||||
|
return self.fetch_dry_run_order(order_id)
|
||||||
|
|
||||||
|
return self._fetch_stop_order_fallback(order_id, pair)
|
||||||
|
|
||||||
|
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
||||||
|
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Bitpanda exchange subclass"""
|
"""Bitpanda exchange subclass"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
@@ -34,5 +34,5 @@ class Bitpanda(Exchange):
|
|||||||
:param pair: Pair the order is for
|
:param pair: Pair the order is for
|
||||||
:param since: datetime object of the order creation time. Assumes object is in UTC.
|
:param since: datetime object of the order creation time. Assumes object is in UTC.
|
||||||
"""
|
"""
|
||||||
params = {"to": int(datetime.now(timezone.utc).timestamp() * 1000)}
|
params = {"to": int(datetime.now(UTC).timestamp() * 1000)}
|
||||||
return super().get_trades_for_order(order_id, pair, since, params)
|
return super().get_trades_for_order(order_id, pair, since, params)
|
||||||
|
|||||||
+31
-18
@@ -1,8 +1,5 @@
|
|||||||
"""Bybit exchange subclass"""
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
@@ -12,6 +9,7 @@ from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalExcep
|
|||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||||
|
from freqtrade.misc import deep_merge_dicts
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -64,9 +62,9 @@ class Bybit(Exchange):
|
|||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
# TradingMode.SPOT always supported and not required in this list
|
(TradingMode.SPOT, MarginMode.NONE),
|
||||||
|
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||||
# (TradingMode.FUTURES, MarginMode.CROSS),
|
# (TradingMode.FUTURES, MarginMode.CROSS),
|
||||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -76,14 +74,11 @@ class Bybit(Exchange):
|
|||||||
config = {}
|
config = {}
|
||||||
if self.trading_mode == TradingMode.SPOT:
|
if self.trading_mode == TradingMode.SPOT:
|
||||||
config.update({"options": {"defaultType": "spot"}})
|
config.update({"options": {"defaultType": "spot"}})
|
||||||
config.update(super()._ccxt_config)
|
elif self.trading_mode == TradingMode.FUTURES:
|
||||||
|
config.update({"options": {"defaultSettle": self._config["stake_currency"]}})
|
||||||
|
config = deep_merge_dicts(config, super()._ccxt_config)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def market_is_future(self, market: dict[str, Any]) -> bool:
|
|
||||||
main = super().market_is_future(market)
|
|
||||||
# For ByBit, we'll only support USDT markets for now.
|
|
||||||
return main and market["settle"] == "USDT"
|
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def additional_exchange_init(self) -> None:
|
def additional_exchange_init(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -182,18 +177,36 @@ class Bybit(Exchange):
|
|||||||
PERPETUAL:
|
PERPETUAL:
|
||||||
bybit:
|
bybit:
|
||||||
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
|
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
|
USDT:
|
||||||
|
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
|
||||||
|
USDC:
|
||||||
|
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#c
|
||||||
|
|
||||||
Long:
|
Long USDT:
|
||||||
|
Liquidation Price = (
|
||||||
|
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||||
|
- (Extra Margin Added/Contract Quantity))
|
||||||
|
Short USDT:
|
||||||
|
Liquidation Price = (
|
||||||
|
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||||
|
+ (Extra Margin Added/Contract Quantity))
|
||||||
|
|
||||||
|
Long USDC:
|
||||||
Liquidation Price = (
|
Liquidation Price = (
|
||||||
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
Position Entry Price - [
|
||||||
- (Extra Margin Added/Contract Quantity))
|
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
|
||||||
Short:
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Short USDC:
|
||||||
Liquidation Price = (
|
Liquidation Price = (
|
||||||
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
Position Entry Price + [
|
||||||
+ (Extra Margin Added/Contract Quantity))
|
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
Implementation Note: Extra margin is currently not used.
|
Implementation Note: Extra margin is currently not used.
|
||||||
|
Due to this - the liquidation formula between USDT and USDC is the same.
|
||||||
|
|
||||||
:param pair: Pair to calculate liquidation price for
|
:param pair: Pair to calculate liquidation price for
|
||||||
:param open_rate: Entry price of position
|
:param open_rate: Entry price of position
|
||||||
|
|||||||
@@ -46,10 +46,9 @@ BAD_EXCHANGES = {
|
|||||||
|
|
||||||
MAP_EXCHANGE_CHILDCLASS = {
|
MAP_EXCHANGE_CHILDCLASS = {
|
||||||
"binanceus": "binance",
|
"binanceus": "binance",
|
||||||
"binanceje": "binance",
|
|
||||||
"binanceusdm": "binance",
|
"binanceusdm": "binance",
|
||||||
"okex": "okx",
|
"okex": "okx",
|
||||||
"myokx": "okx",
|
"okxus": "okx",
|
||||||
"gateio": "gate",
|
"gateio": "gate",
|
||||||
"huboi": "htx",
|
"huboi": "htx",
|
||||||
}
|
}
|
||||||
@@ -64,6 +63,7 @@ SUPPORTED_EXCHANGES = [
|
|||||||
"hyperliquid",
|
"hyperliquid",
|
||||||
"kraken",
|
"kraken",
|
||||||
"okx",
|
"okx",
|
||||||
|
"myokx",
|
||||||
]
|
]
|
||||||
|
|
||||||
# either the main, or replacement methods (array) is required
|
# either the main, or replacement methods (array) is required
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import logging
|
|||||||
import signal
|
import signal
|
||||||
from collections.abc import Coroutine, Generator
|
from collections.abc import Coroutine, Generator
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import UTC, datetime, timedelta
|
||||||
from math import floor, isnan
|
from math import floor, isnan
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Literal, TypeGuard, TypeVar
|
from typing import Any, Literal, TypeGuard, TypeVar
|
||||||
@@ -137,6 +137,7 @@ class Exchange:
|
|||||||
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
||||||
"ohlcv_partial_candle": True,
|
"ohlcv_partial_candle": True,
|
||||||
"ohlcv_require_since": False,
|
"ohlcv_require_since": False,
|
||||||
|
"always_require_api_keys": False, # purge API keys for Dry-run. Must default to false.
|
||||||
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
||||||
"ohlcv_volume_currency": "base", # "base" or "quote"
|
"ohlcv_volume_currency": "base", # "base" or "quote"
|
||||||
"tickers_have_quoteVolume": True,
|
"tickers_have_quoteVolume": True,
|
||||||
@@ -168,7 +169,8 @@ class Exchange:
|
|||||||
_ft_has_futures: FtHas = {}
|
_ft_has_futures: FtHas = {}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
# TradingMode.SPOT always supported and not required in this list
|
# Non-defined exchanges only support spot mode.
|
||||||
|
(TradingMode.SPOT, MarginMode.NONE),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -197,7 +199,26 @@ class Exchange:
|
|||||||
self.loop = self._init_async_loop()
|
self.loop = self._init_async_loop()
|
||||||
self._config: Config = {}
|
self._config: Config = {}
|
||||||
|
|
||||||
|
# Leverage properties
|
||||||
|
self.trading_mode: TradingMode = TradingMode(
|
||||||
|
config.get("trading_mode", self._supported_trading_mode_margin_pairs[0][0])
|
||||||
|
)
|
||||||
|
self.margin_mode: MarginMode = MarginMode(
|
||||||
|
MarginMode(config.get("margin_mode"))
|
||||||
|
if config.get("margin_mode")
|
||||||
|
else self._supported_trading_mode_margin_pairs[0][1]
|
||||||
|
)
|
||||||
|
config["trading_mode"] = self.trading_mode
|
||||||
|
config["margin_mode"] = self.margin_mode
|
||||||
|
config["candle_type_def"] = CandleType.get_default(self.trading_mode)
|
||||||
self._config.update(config)
|
self._config.update(config)
|
||||||
|
self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
|
||||||
|
|
||||||
|
exchange_conf: ExchangeConfig = exchange_config if exchange_config else config["exchange"]
|
||||||
|
|
||||||
|
# Deep merge ft_has with default ft_has options
|
||||||
|
# Must be called before ft_has is used.
|
||||||
|
self.build_ft_has(exchange_conf)
|
||||||
|
|
||||||
# Holds last candle refreshed time of each pair
|
# Holds last candle refreshed time of each pair
|
||||||
self._pairs_last_refresh_time: dict[PairWithTimeframe, int] = {}
|
self._pairs_last_refresh_time: dict[PairWithTimeframe, int] = {}
|
||||||
@@ -227,33 +248,17 @@ class Exchange:
|
|||||||
if config["dry_run"]:
|
if config["dry_run"]:
|
||||||
logger.info("Instance is running with dry_run enabled")
|
logger.info("Instance is running with dry_run enabled")
|
||||||
logger.info(f"Using CCXT {ccxt.__version__}")
|
logger.info(f"Using CCXT {ccxt.__version__}")
|
||||||
exchange_conf: dict[str, Any] = exchange_config if exchange_config else config["exchange"]
|
|
||||||
remove_exchange_credentials(exchange_conf, config.get("dry_run", False))
|
|
||||||
self.log_responses = exchange_conf.get("log_responses", False)
|
|
||||||
|
|
||||||
# Leverage properties
|
# Don't remove exchange credentials for dry-run or if always_require_api_keys is set
|
||||||
self.trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT)
|
remove_exchange_credentials(
|
||||||
self.margin_mode: MarginMode = (
|
exchange_conf,
|
||||||
MarginMode(config.get("margin_mode")) if config.get("margin_mode") else MarginMode.NONE
|
not self._ft_has["always_require_api_keys"] and config.get("dry_run", False),
|
||||||
)
|
)
|
||||||
self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
|
self.log_responses = exchange_conf.get("log_responses", False)
|
||||||
|
|
||||||
# Deep merge ft_has with default ft_has options
|
|
||||||
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
|
||||||
self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
|
|
||||||
if exchange_conf.get("_ft_has_params"):
|
|
||||||
self._ft_has = deep_merge_dicts(exchange_conf.get("_ft_has_params"), self._ft_has)
|
|
||||||
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
|
|
||||||
|
|
||||||
# Assign this directly for easy access
|
# Assign this directly for easy access
|
||||||
self._ohlcv_partial_candle = self._ft_has["ohlcv_partial_candle"]
|
self._ohlcv_partial_candle = self._ft_has["ohlcv_partial_candle"]
|
||||||
|
|
||||||
self._max_trades_limit = self._ft_has["trades_limit"]
|
|
||||||
|
|
||||||
self._trades_pagination = self._ft_has["trades_pagination"]
|
|
||||||
self._trades_pagination_arg = self._ft_has["trades_pagination_arg"]
|
|
||||||
|
|
||||||
# Initialize ccxt objects
|
# Initialize ccxt objects
|
||||||
ccxt_config = self._ccxt_config
|
ccxt_config = self._ccxt_config
|
||||||
ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_config", {}), ccxt_config)
|
ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_config", {}), ccxt_config)
|
||||||
@@ -289,10 +294,6 @@ class Exchange:
|
|||||||
# Initial markets load
|
# Initial markets load
|
||||||
self.reload_markets(True, load_leverage_tiers=False)
|
self.reload_markets(True, load_leverage_tiers=False)
|
||||||
self.validate_config(config)
|
self.validate_config(config)
|
||||||
self._startup_candle_count: int = config.get("startup_candle_count", 0)
|
|
||||||
self.required_candle_call_count = self.validate_required_startup_candles(
|
|
||||||
self._startup_candle_count, config.get("timeframe", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
||||||
self.fill_leverage_tiers()
|
self.fill_leverage_tiers()
|
||||||
@@ -331,6 +332,12 @@ class Exchange:
|
|||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
return loop
|
return loop
|
||||||
|
|
||||||
|
def _set_startup_candle_count(self, config: Config) -> None:
|
||||||
|
self._startup_candle_count: int = config.get("startup_candle_count", 0)
|
||||||
|
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||||
|
self._startup_candle_count, config.get("timeframe", "")
|
||||||
|
)
|
||||||
|
|
||||||
def validate_config(self, config: Config) -> None:
|
def validate_config(self, config: Config) -> None:
|
||||||
# Check if timeframe is available
|
# Check if timeframe is available
|
||||||
self.validate_timeframes(config.get("timeframe"))
|
self.validate_timeframes(config.get("timeframe"))
|
||||||
@@ -345,6 +352,8 @@ class Exchange:
|
|||||||
self.validate_orderflow(config["exchange"])
|
self.validate_orderflow(config["exchange"])
|
||||||
self.validate_freqai(config)
|
self.validate_freqai(config)
|
||||||
|
|
||||||
|
self._set_startup_candle_count(config)
|
||||||
|
|
||||||
def _init_ccxt(
|
def _init_ccxt(
|
||||||
self, exchange_config: dict[str, Any], sync: bool, ccxt_kwargs: dict[str, Any]
|
self, exchange_config: dict[str, Any], sync: bool, ccxt_kwargs: dict[str, Any]
|
||||||
) -> ccxt.Exchange:
|
) -> ccxt.Exchange:
|
||||||
@@ -657,7 +666,7 @@ class Exchange:
|
|||||||
if isinstance(markets, Exception):
|
if isinstance(markets, Exception):
|
||||||
raise markets
|
raise markets
|
||||||
return None
|
return None
|
||||||
except asyncio.TimeoutError as e:
|
except TimeoutError as e:
|
||||||
logger.warning("Could not load markets. Reason: %s", e)
|
logger.warning("Could not load markets. Reason: %s", e)
|
||||||
raise TemporaryError from e
|
raise TemporaryError from e
|
||||||
|
|
||||||
@@ -881,6 +890,20 @@ class Exchange:
|
|||||||
f"Freqtrade does not support '{mm_value}' '{trading_mode}' on {self.name}."
|
f"Freqtrade does not support '{mm_value}' '{trading_mode}' on {self.name}."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def build_ft_has(self, exchange_conf: ExchangeConfig) -> None:
|
||||||
|
"""
|
||||||
|
Deep merge ft_has with default ft_has options
|
||||||
|
and with exchange_conf._ft_has_params if available.
|
||||||
|
This is called on initialization of the exchange object.
|
||||||
|
It must be called before ft_has is used.
|
||||||
|
"""
|
||||||
|
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
|
||||||
|
if exchange_conf.get("_ft_has_params"):
|
||||||
|
self._ft_has = deep_merge_dicts(exchange_conf.get("_ft_has_params"), self._ft_has)
|
||||||
|
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
|
||||||
|
|
||||||
def get_option(self, param: str, default: Any | None = None) -> Any:
|
def get_option(self, param: str, default: Any | None = None) -> Any:
|
||||||
"""
|
"""
|
||||||
Get parameter value from _ft_has
|
Get parameter value from _ft_has
|
||||||
@@ -2208,7 +2231,7 @@ class Exchange:
|
|||||||
_params = params if params else {}
|
_params = params if params else {}
|
||||||
my_trades = self._api.fetch_my_trades(
|
my_trades = self._api.fetch_my_trades(
|
||||||
pair,
|
pair,
|
||||||
int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000),
|
int((since.replace(tzinfo=UTC).timestamp() - 5) * 1000),
|
||||||
params=_params,
|
params=_params,
|
||||||
)
|
)
|
||||||
matched_trades = [trade for trade in my_trades if trade["order"] == order_id]
|
matched_trades = [trade for trade in my_trades if trade["order"] == order_id]
|
||||||
@@ -2584,10 +2607,12 @@ class Exchange:
|
|||||||
if ticks and cache:
|
if ticks and cache:
|
||||||
idx = -2 if drop_incomplete and len(ticks) > 1 else -1
|
idx = -2 if drop_incomplete and len(ticks) > 1 else -1
|
||||||
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0]
|
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0]
|
||||||
# keeping parsed dataframe in cache
|
has_cache = cache and (pair, timeframe, c_type) in self._klines
|
||||||
|
# in case of existing cache, fill_missing happens after concatenation
|
||||||
ohlcv_df = ohlcv_to_dataframe(
|
ohlcv_df = ohlcv_to_dataframe(
|
||||||
ticks, timeframe, pair=pair, fill_missing=True, drop_incomplete=drop_incomplete
|
ticks, timeframe, pair=pair, fill_missing=not has_cache, drop_incomplete=drop_incomplete
|
||||||
)
|
)
|
||||||
|
# keeping parsed dataframe in cache
|
||||||
if cache:
|
if cache:
|
||||||
if (pair, timeframe, c_type) in self._klines:
|
if (pair, timeframe, c_type) in self._klines:
|
||||||
old = self._klines[(pair, timeframe, c_type)]
|
old = self._klines[(pair, timeframe, c_type)]
|
||||||
@@ -2995,7 +3020,7 @@ class Exchange:
|
|||||||
returns: List of dicts containing trades, the next iteration value (new "since" or trade_id)
|
returns: List of dicts containing trades, the next iteration value (new "since" or trade_id)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
trades_limit = self._max_trades_limit
|
trades_limit = self._ft_has["trades_limit"]
|
||||||
# fetch trades asynchronously
|
# fetch trades asynchronously
|
||||||
if params:
|
if params:
|
||||||
logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
|
logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
|
||||||
@@ -3039,7 +3064,7 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
if not trades:
|
if not trades:
|
||||||
return None
|
return None
|
||||||
if self._trades_pagination == "id":
|
if self._ft_has["trades_pagination"] == "id":
|
||||||
return trades[-1].get("id")
|
return trades[-1].get("id")
|
||||||
else:
|
else:
|
||||||
return trades[-1].get("timestamp")
|
return trades[-1].get("timestamp")
|
||||||
@@ -3057,7 +3082,7 @@ class Exchange:
|
|||||||
) -> tuple[str, list[list]]:
|
) -> tuple[str, list[list]]:
|
||||||
"""
|
"""
|
||||||
Asynchronously gets trade history using fetch_trades
|
Asynchronously gets trade history using fetch_trades
|
||||||
use this when exchange uses id-based iteration (check `self._trades_pagination`)
|
use this when exchange uses id-based iteration (check `self._ft_has["trades_pagination"]`)
|
||||||
:param pair: Pair to fetch trade data for
|
:param pair: Pair to fetch trade data for
|
||||||
:param since: Since as integer timestamp in milliseconds
|
:param since: Since as integer timestamp in milliseconds
|
||||||
:param until: Until as integer timestamp in milliseconds
|
:param until: Until as integer timestamp in milliseconds
|
||||||
@@ -3083,7 +3108,7 @@ class Exchange:
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
t, from_id_next = await self._async_fetch_trades(
|
t, from_id_next = await self._async_fetch_trades(
|
||||||
pair, params={self._trades_pagination_arg: from_id}
|
pair, params={self._ft_has["trades_pagination_arg"]: from_id}
|
||||||
)
|
)
|
||||||
if t:
|
if t:
|
||||||
trades.extend(t[x])
|
trades.extend(t[x])
|
||||||
@@ -3111,7 +3136,7 @@ class Exchange:
|
|||||||
) -> tuple[str, list[list]]:
|
) -> tuple[str, list[list]]:
|
||||||
"""
|
"""
|
||||||
Asynchronously gets trade history using fetch_trades,
|
Asynchronously gets trade history using fetch_trades,
|
||||||
when the exchange uses time-based iteration (check `self._trades_pagination`)
|
when the exchange uses time-based iteration (check `self._ft_has["trades_pagination"]`)
|
||||||
:param pair: Pair to fetch trade data for
|
:param pair: Pair to fetch trade data for
|
||||||
:param since: Since as integer timestamp in milliseconds
|
:param since: Since as integer timestamp in milliseconds
|
||||||
:param until: Until as integer timestamp in milliseconds
|
:param until: Until as integer timestamp in milliseconds
|
||||||
@@ -3165,9 +3190,9 @@ class Exchange:
|
|||||||
until = ccxt.Exchange.milliseconds()
|
until = ccxt.Exchange.milliseconds()
|
||||||
logger.debug(f"Exchange milliseconds: {until}")
|
logger.debug(f"Exchange milliseconds: {until}")
|
||||||
|
|
||||||
if self._trades_pagination == "time":
|
if self._ft_has["trades_pagination"] == "time":
|
||||||
return await self._async_get_trade_history_time(pair=pair, since=since, until=until)
|
return await self._async_get_trade_history_time(pair=pair, since=since, until=until)
|
||||||
elif self._trades_pagination == "id":
|
elif self._ft_has["trades_pagination"] == "id":
|
||||||
return await self._async_get_trade_history_id(
|
return await self._async_get_trade_history_id(
|
||||||
pair=pair, since=since, until=until, from_id=from_id
|
pair=pair, since=since, until=until, from_id=from_id
|
||||||
)
|
)
|
||||||
@@ -3335,7 +3360,7 @@ class Exchange:
|
|||||||
if not filename.parent.is_dir():
|
if not filename.parent.is_dir():
|
||||||
filename.parent.mkdir(parents=True)
|
filename.parent.mkdir(parents=True)
|
||||||
data = {
|
data = {
|
||||||
"updated": datetime.now(timezone.utc),
|
"updated": datetime.now(UTC),
|
||||||
"data": tiers,
|
"data": tiers,
|
||||||
}
|
}
|
||||||
file_dump_json(filename, data)
|
file_dump_json(filename, data)
|
||||||
@@ -3357,7 +3382,7 @@ class Exchange:
|
|||||||
updated = tiers.get("updated")
|
updated = tiers.get("updated")
|
||||||
if updated:
|
if updated:
|
||||||
updated_dt = parser.parse(updated)
|
updated_dt = parser.parse(updated)
|
||||||
if updated_dt < datetime.now(timezone.utc) - cache_time:
|
if updated_dt < datetime.now(UTC) - cache_time:
|
||||||
logger.info("Cached leverage tiers are outdated. Will update.")
|
logger.info("Cached leverage tiers are outdated. Will update.")
|
||||||
return None
|
return None
|
||||||
return tiers.get("data")
|
return tiers.get("data")
|
||||||
@@ -3416,17 +3441,26 @@ class Exchange:
|
|||||||
# Find the appropriate tier based on stake_amount
|
# Find the appropriate tier based on stake_amount
|
||||||
prior_max_lev = None
|
prior_max_lev = None
|
||||||
for tier in pair_tiers:
|
for tier in pair_tiers:
|
||||||
|
# Adjust notional by leverage to do a proper comparison
|
||||||
min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"])
|
min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"])
|
||||||
max_stake = tier["maxNotional"] / tier["maxLeverage"]
|
max_stake = tier["maxNotional"] / tier["maxLeverage"]
|
||||||
prior_max_lev = tier["maxLeverage"]
|
prior_max_lev = tier["maxLeverage"]
|
||||||
# Adjust notional by leverage to do a proper comparison
|
|
||||||
if min_stake <= stake_amount <= max_stake:
|
if min_stake <= stake_amount <= max_stake:
|
||||||
return tier["maxLeverage"]
|
return tier["maxLeverage"]
|
||||||
|
if stake_amount < min_stake and stake_amount <= max_stake:
|
||||||
|
# TODO: Remove this warning eventually
|
||||||
|
# Code could be simplified by removing the check for min-stake in the above
|
||||||
|
# condition, making this branch unnecessary.
|
||||||
|
logger.warning(
|
||||||
|
f"Fallback to next higher leverage tier for {pair}, stake: {stake_amount}, "
|
||||||
|
f"min_stake: {min_stake}."
|
||||||
|
)
|
||||||
|
return tier["maxLeverage"]
|
||||||
|
|
||||||
# else: # if on the last tier
|
# else: # if on the last tier
|
||||||
if stake_amount > max_stake:
|
if stake_amount > max_stake:
|
||||||
# If stake is > than max tradeable amount
|
# If stake is > than max tradeable amount
|
||||||
raise InvalidOrderException(f"Amount {stake_amount} too high for {pair}")
|
raise InvalidOrderException(f"Stake amount {stake_amount} too high for {pair}")
|
||||||
|
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"Looped through all tiers without finding a max leverage for {pair}. "
|
f"Looped through all tiers without finding a max leverage for {pair}. "
|
||||||
@@ -3572,7 +3606,7 @@ class Exchange:
|
|||||||
mark_price_type = CandleType.from_string(self._ft_has["mark_ohlcv_price"])
|
mark_price_type = CandleType.from_string(self._ft_has["mark_ohlcv_price"])
|
||||||
|
|
||||||
if not close_date:
|
if not close_date:
|
||||||
close_date = datetime.now(timezone.utc)
|
close_date = datetime.now(UTC)
|
||||||
since_ms = dt_ts(timeframe_to_prev_date(timeframe, open_date))
|
since_ms = dt_ts(timeframe_to_prev_date(timeframe, open_date))
|
||||||
|
|
||||||
mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type)
|
mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class FtHas(TypedDict, total=False):
|
|||||||
ohlcv_require_since: bool
|
ohlcv_require_since: bool
|
||||||
ohlcv_volume_currency: str
|
ohlcv_volume_currency: str
|
||||||
ohlcv_candle_limit_per_timeframe: dict[str, int]
|
ohlcv_candle_limit_per_timeframe: dict[str, int]
|
||||||
|
always_require_api_keys: bool
|
||||||
# Tickers
|
# Tickers
|
||||||
tickers_have_quoteVolume: bool
|
tickers_have_quoteVolume: bool
|
||||||
tickers_have_percentage: bool
|
tickers_have_percentage: bool
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Exchange support utils
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import UTC, datetime, timedelta
|
||||||
from math import ceil, floor, isnan
|
from math import ceil, floor, isnan
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ from freqtrade.exchange.common import (
|
|||||||
SUPPORTED_EXCHANGES,
|
SUPPORTED_EXCHANGES,
|
||||||
)
|
)
|
||||||
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_minutes, timeframe_to_prev_date
|
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_minutes, timeframe_to_prev_date
|
||||||
from freqtrade.ft_types import ValidExchangesType
|
from freqtrade.ft_types import TradeModeType, ValidExchangesType
|
||||||
from freqtrade.util import FtPrecise
|
from freqtrade.util import FtPrecise
|
||||||
|
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ def _build_exchange_list_entry(
|
|||||||
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
|
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
|
||||||
}
|
}
|
||||||
if resolved := exchangeClasses.get(mapped_exchange_name):
|
if resolved := exchangeClasses.get(mapped_exchange_name):
|
||||||
supported_modes = [{"trading_mode": "spot", "margin_mode": ""}] + [
|
supported_modes: list[TradeModeType] = [
|
||||||
{"trading_mode": tm.value, "margin_mode": mm.value}
|
{"trading_mode": tm.value, "margin_mode": mm.value}
|
||||||
for tm, mm in resolved["class"]._supported_trading_mode_margin_pairs
|
for tm, mm in resolved["class"]._supported_trading_mode_margin_pairs
|
||||||
]
|
]
|
||||||
@@ -148,7 +148,7 @@ def date_minus_candles(timeframe: str, candle_count: int, date: datetime | None
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if not date:
|
if not date:
|
||||||
date = datetime.now(timezone.utc)
|
date = datetime.now(UTC)
|
||||||
|
|
||||||
tf_min = timeframe_to_minutes(timeframe)
|
tf_min = timeframe_to_minutes(timeframe)
|
||||||
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)
|
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)
|
||||||
@@ -213,9 +213,9 @@ def amount_to_precision(
|
|||||||
amount = float(
|
amount = float(
|
||||||
decimal_to_precision(
|
decimal_to_precision(
|
||||||
amount,
|
amount,
|
||||||
rounding_mode=TRUNCATE,
|
TRUNCATE, # rounding_mode
|
||||||
precision=precision,
|
precision, # numPrecisionDigits
|
||||||
counting_mode=precisionMode,
|
precisionMode, # counting_mode
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -311,11 +311,11 @@ def price_to_precision(
|
|||||||
return float(
|
return float(
|
||||||
decimal_to_precision(
|
decimal_to_precision(
|
||||||
price,
|
price,
|
||||||
rounding_mode=rounding_mode,
|
rounding_mode, # rounding mode
|
||||||
precision=int(price_precision)
|
int(price_precision)
|
||||||
if precisionMode != TICK_SIZE
|
if precisionMode != TICK_SIZE
|
||||||
else price_precision,
|
else price_precision, # numPrecisionDigits
|
||||||
counting_mode=precisionMode,
|
precisionMode, # counting_mode
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
from ccxt import ROUND_DOWN, ROUND_UP
|
from ccxt import ROUND_DOWN, ROUND_UP
|
||||||
@@ -59,7 +59,7 @@ def timeframe_to_prev_date(timeframe: str, date: datetime | None = None) -> date
|
|||||||
:returns: date of previous candle (with utc timezone)
|
:returns: date of previous candle (with utc timezone)
|
||||||
"""
|
"""
|
||||||
if not date:
|
if not date:
|
||||||
date = datetime.now(timezone.utc)
|
date = datetime.now(UTC)
|
||||||
|
|
||||||
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_DOWN) // 1000
|
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_DOWN) // 1000
|
||||||
return dt_from_ts(new_timestamp)
|
return dt_from_ts(new_timestamp)
|
||||||
@@ -73,6 +73,6 @@ def timeframe_to_next_date(timeframe: str, date: datetime | None = None) -> date
|
|||||||
:returns: date of next candle (with utc timezone)
|
:returns: date of next candle (with utc timezone)
|
||||||
"""
|
"""
|
||||||
if not date:
|
if not date:
|
||||||
date = datetime.now(timezone.utc)
|
date = datetime.now(UTC)
|
||||||
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_UP) // 1000
|
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_UP) // 1000
|
||||||
return dt_from_ts(new_timestamp)
|
return dt_from_ts(new_timestamp)
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ class Gate(Exchange):
|
|||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
# TradingMode.SPOT always supported and not required in this list
|
(TradingMode.SPOT, MarginMode.NONE),
|
||||||
# (TradingMode.MARGIN, MarginMode.CROSS),
|
# (TradingMode.MARGIN, MarginMode.CROSS),
|
||||||
# (TradingMode.FUTURES, MarginMode.CROSS),
|
# (TradingMode.FUTURES, MarginMode.CROSS),
|
||||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||||
]
|
]
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class Hyperliquid(Exchange):
|
|||||||
"stoploss_on_exchange": False,
|
"stoploss_on_exchange": False,
|
||||||
"exchange_has_overrides": {"fetchTrades": False},
|
"exchange_has_overrides": {"fetchTrades": False},
|
||||||
"marketOrderRequiresPrice": True,
|
"marketOrderRequiresPrice": True,
|
||||||
|
"ws_enabled": True,
|
||||||
}
|
}
|
||||||
_ft_has_futures: FtHas = {
|
_ft_has_futures: FtHas = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
@@ -40,7 +41,8 @@ class Hyperliquid(Exchange):
|
|||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
(TradingMode.SPOT, MarginMode.NONE),
|
||||||
|
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class Kraken(Exchange):
|
|||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
# TradingMode.SPOT always supported and not required in this list
|
(TradingMode.SPOT, MarginMode.NONE),
|
||||||
# (TradingMode.MARGIN, MarginMode.CROSS),
|
# (TradingMode.MARGIN, MarginMode.CROSS),
|
||||||
# (TradingMode.FUTURES, MarginMode.CROSS)
|
# (TradingMode.FUTURES, MarginMode.CROSS)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.exchange.exchange_types import FtHas
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Luno(Exchange):
|
||||||
|
"""
|
||||||
|
Luno 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_has_history": False, # Only provides the last 1000 candles
|
||||||
|
"always_require_api_keys": True, # Requires API keys to fetch candles
|
||||||
|
"trades_has_history": False, # Only the last 24h are available
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
# from freqtrade.enums import MarginMode, TradingMode
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.exchange.exchange_types import FtHas
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Modetrade(Exchange):
|
||||||
|
"""
|
||||||
|
MOdetrade 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 = {
|
||||||
|
"always_require_api_keys": True, # Requires API keys to fetch candles
|
||||||
|
}
|
||||||
|
|
||||||
|
# _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
|
# (TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||||
|
# ]
|
||||||
@@ -49,7 +49,7 @@ class Okx(Exchange):
|
|||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
# TradingMode.SPOT always supported and not required in this list
|
(TradingMode.SPOT, MarginMode.NONE),
|
||||||
# (TradingMode.MARGIN, MarginMode.CROSS),
|
# (TradingMode.MARGIN, MarginMode.CROSS),
|
||||||
# (TradingMode.FUTURES, MarginMode.CROSS),
|
# (TradingMode.FUTURES, MarginMode.CROSS),
|
||||||
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||||
@@ -287,3 +287,14 @@ class Okx(Exchange):
|
|||||||
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
|
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
|
||||||
orders.extend(orders_open)
|
orders.extend(orders_open)
|
||||||
return orders
|
return orders
|
||||||
|
|
||||||
|
|
||||||
|
class MyOkx(Okx):
|
||||||
|
"""
|
||||||
|
MyOkx exchange class.
|
||||||
|
Minimal adjustment to disable futures trading for the EU subsidiary of Okx
|
||||||
|
"""
|
||||||
|
|
||||||
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
|
(TradingMode.SPOT, MarginMode.NONE),
|
||||||
|
]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import importlib
|
|||||||
import logging
|
import logging
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
pair, refresh=False, side="exit", is_short=trade.is_short
|
pair, refresh=False, side="exit", is_short=trade.is_short
|
||||||
)
|
)
|
||||||
|
|
||||||
now = datetime.now(timezone.utc).timestamp()
|
now = datetime.now(UTC).timestamp()
|
||||||
trade_duration = int((now - trade.open_date_utc.timestamp()) / self.base_tf_seconds)
|
trade_duration = int((now - trade.open_date_utc.timestamp()) / self.base_tf_seconds)
|
||||||
current_profit = trade.calc_profit_ratio(current_rate)
|
current_profit = trade.calc_profit_ratio(current_rate)
|
||||||
if trade.is_short:
|
if trade.is_short:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import UTC, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, TypedDict
|
from typing import Any, TypedDict
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ class FreqaiDataDrawer:
|
|||||||
if metric not in self.metric_tracker[pair]:
|
if metric not in self.metric_tracker[pair]:
|
||||||
self.metric_tracker[pair][metric] = {"timestamp": [], "value": []}
|
self.metric_tracker[pair][metric] = {"timestamp": [], "value": []}
|
||||||
|
|
||||||
timestamp = int(datetime.now(timezone.utc).timestamp())
|
timestamp = int(datetime.now(UTC).timestamp())
|
||||||
self.metric_tracker[pair][metric]["value"].append(value)
|
self.metric_tracker[pair][metric]["value"].append(value)
|
||||||
self.metric_tracker[pair][metric]["timestamp"].append(timestamp)
|
self.metric_tracker[pair][metric]["timestamp"].append(timestamp)
|
||||||
|
|
||||||
@@ -493,7 +493,7 @@ class FreqaiDataDrawer:
|
|||||||
|
|
||||||
dk.data["data_path"] = str(dk.data_path)
|
dk.data["data_path"] = str(dk.data_path)
|
||||||
dk.data["model_filename"] = str(dk.model_filename)
|
dk.data["model_filename"] = str(dk.model_filename)
|
||||||
dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns)
|
dk.data["training_features_list"] = dk.training_features_list
|
||||||
dk.data["label_list"] = dk.label_list
|
dk.data["label_list"] = dk.label_list
|
||||||
|
|
||||||
with (save_path / f"{dk.model_filename}_{METADATA}.json").open("w") as fp:
|
with (save_path / f"{dk.model_filename}_{METADATA}.json").open("w") as fp:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import inspect
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -341,7 +341,7 @@ class FreqaiDataKitchen:
|
|||||||
full_timerange = TimeRange.parse_timerange(tr)
|
full_timerange = TimeRange.parse_timerange(tr)
|
||||||
config_timerange = TimeRange.parse_timerange(self.config["timerange"])
|
config_timerange = TimeRange.parse_timerange(self.config["timerange"])
|
||||||
if config_timerange.stopts == 0:
|
if config_timerange.stopts == 0:
|
||||||
config_timerange.stopts = int(datetime.now(tz=timezone.utc).timestamp())
|
config_timerange.stopts = int(datetime.now(tz=UTC).timestamp())
|
||||||
timerange_train = copy.deepcopy(full_timerange)
|
timerange_train = copy.deepcopy(full_timerange)
|
||||||
timerange_backtest = copy.deepcopy(full_timerange)
|
timerange_backtest = copy.deepcopy(full_timerange)
|
||||||
|
|
||||||
@@ -525,7 +525,7 @@ class FreqaiDataKitchen:
|
|||||||
:return:
|
:return:
|
||||||
bool = If the model is expired or not.
|
bool = If the model is expired or not.
|
||||||
"""
|
"""
|
||||||
time = datetime.now(tz=timezone.utc).timestamp()
|
time = datetime.now(tz=UTC).timestamp()
|
||||||
elapsed_time = (time - trained_timestamp) / 3600 # hours
|
elapsed_time = (time - trained_timestamp) / 3600 # hours
|
||||||
max_time = self.freqai_config.get("expiration_hours", 0)
|
max_time = self.freqai_config.get("expiration_hours", 0)
|
||||||
if max_time > 0:
|
if max_time > 0:
|
||||||
@@ -536,7 +536,7 @@ class FreqaiDataKitchen:
|
|||||||
def check_if_new_training_required(
|
def check_if_new_training_required(
|
||||||
self, trained_timestamp: int
|
self, trained_timestamp: int
|
||||||
) -> tuple[bool, TimeRange, TimeRange]:
|
) -> tuple[bool, TimeRange, TimeRange]:
|
||||||
time = datetime.now(tz=timezone.utc).timestamp()
|
time = datetime.now(tz=UTC).timestamp()
|
||||||
trained_timerange = TimeRange()
|
trained_timerange = TimeRange()
|
||||||
data_load_timerange = TimeRange()
|
data_load_timerange = TimeRange()
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ class IFreqaiModel(ABC):
|
|||||||
|
|
||||||
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config)
|
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config)
|
||||||
# set current candle to arbitrary historical date
|
# set current candle to arbitrary historical date
|
||||||
self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=timezone.utc)
|
self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=UTC)
|
||||||
self.dd.current_candle = self.current_candle
|
self.dd.current_candle = self.current_candle
|
||||||
self.scanning = False
|
self.scanning = False
|
||||||
self.ft_params = self.freqai_info["feature_parameters"]
|
self.ft_params = self.freqai_info["feature_parameters"]
|
||||||
@@ -514,12 +514,7 @@ class IFreqaiModel(ABC):
|
|||||||
current coin/bot loop
|
current coin/bot loop
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if "training_features_list_raw" in dk.data:
|
if dk.training_features_list != dk.data["training_features_list"]:
|
||||||
feature_list = dk.data["training_features_list_raw"]
|
|
||||||
else:
|
|
||||||
feature_list = dk.data["training_features_list"]
|
|
||||||
|
|
||||||
if dk.training_features_list != feature_list:
|
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"Trying to access pretrained model with `identifier` "
|
"Trying to access pretrained model with `identifier` "
|
||||||
"but found different features furnished by current strategy. "
|
"but found different features furnished by current strategy. "
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ def get_required_data_timerange(config: Config) -> TimeRange:
|
|||||||
Used to compute the required data download time range
|
Used to compute the required data download time range
|
||||||
for auto data-download in FreqAI
|
for auto data-download in FreqAI
|
||||||
"""
|
"""
|
||||||
time = datetime.now(tz=timezone.utc).timestamp()
|
time = datetime.now(tz=UTC).timestamp()
|
||||||
|
|
||||||
timeframes = config["freqai"]["feature_parameters"].get("include_timeframes")
|
timeframes = config["freqai"]["feature_parameters"].get("include_timeframes")
|
||||||
|
|
||||||
|
|||||||
+76
-73
@@ -5,7 +5,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
|
|||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, time, timedelta, timezone
|
from datetime import UTC, datetime, time, timedelta
|
||||||
from math import isclose
|
from math import isclose
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from time import sleep
|
from time import sleep
|
||||||
@@ -93,14 +93,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Remove credentials from original exchange config to avoid accidental credential exposure
|
# Remove credentials from original exchange config to avoid accidental credential exposure
|
||||||
remove_exchange_credentials(config["exchange"], True)
|
remove_exchange_credentials(config["exchange"], True)
|
||||||
|
|
||||||
|
self.exchange = ExchangeResolver.load_exchange(
|
||||||
|
self.config, exchange_config=exchange_config, load_leverage_tiers=True
|
||||||
|
)
|
||||||
|
|
||||||
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
|
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
|
||||||
|
|
||||||
# Check config consistency here since strategies can set certain options
|
# Check config consistency here since strategies can set certain options
|
||||||
validate_config_consistency(config)
|
validate_config_consistency(config)
|
||||||
|
# Re-validate exchange compatibility
|
||||||
self.exchange = ExchangeResolver.load_exchange(
|
self.exchange.validate_config(self.config)
|
||||||
self.config, exchange_config=exchange_config, load_leverage_tiers=True
|
|
||||||
)
|
|
||||||
|
|
||||||
init_db(self.config["db_url"])
|
init_db(self.config["db_url"])
|
||||||
|
|
||||||
@@ -266,7 +268,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
|
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
|
||||||
current_time=datetime.now(timezone.utc)
|
current_time=datetime.now(UTC)
|
||||||
)
|
)
|
||||||
|
|
||||||
with self._measure_execution:
|
with self._measure_execution:
|
||||||
@@ -296,7 +298,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self._schedule.run_pending()
|
self._schedule.run_pending()
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
self.rpc.process_msg_queue(self.dataprovider._msg_queue)
|
self.rpc.process_msg_queue(self.dataprovider._msg_queue)
|
||||||
self.last_process = datetime.now(timezone.utc)
|
self.last_process = datetime.now(UTC)
|
||||||
|
|
||||||
def process_stopped(self) -> None:
|
def process_stopped(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -421,7 +423,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
except InvalidOrderException as e:
|
except InvalidOrderException as e:
|
||||||
logger.warning(f"Error updating Order {order.order_id} due to {e}.")
|
logger.warning(f"Error updating Order {order.order_id} due to {e}.")
|
||||||
if order.order_date_utc - timedelta(days=5) < datetime.now(timezone.utc):
|
if order.order_date_utc - timedelta(days=5) < datetime.now(UTC):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Order is older than 5 days. Assuming order was fully cancelled."
|
"Order is older than 5 days. Assuming order was fully cancelled."
|
||||||
)
|
)
|
||||||
@@ -755,7 +757,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
|
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
|
||||||
stake_amount, order_tag = self.strategy._adjust_trade_position_internal(
|
stake_amount, order_tag = self.strategy._adjust_trade_position_internal(
|
||||||
trade=trade,
|
trade=trade,
|
||||||
current_time=datetime.now(timezone.utc),
|
current_time=datetime.now(UTC),
|
||||||
current_rate=current_entry_rate,
|
current_rate=current_entry_rate,
|
||||||
current_profit=current_entry_profit,
|
current_profit=current_entry_profit,
|
||||||
min_stake=min_entry_stake,
|
min_stake=min_entry_stake,
|
||||||
@@ -916,7 +918,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
amount=amount,
|
amount=amount,
|
||||||
rate=enter_limit_requested,
|
rate=enter_limit_requested,
|
||||||
time_in_force=time_in_force,
|
time_in_force=time_in_force,
|
||||||
current_time=datetime.now(timezone.utc),
|
current_time=datetime.now(UTC),
|
||||||
entry_tag=enter_tag,
|
entry_tag=enter_tag,
|
||||||
side=trade_side,
|
side=trade_side,
|
||||||
):
|
):
|
||||||
@@ -987,7 +989,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker="maker")
|
fee = self.exchange.get_fee(symbol=pair, taker_or_maker="maker")
|
||||||
base_currency = self.exchange.get_pair_base_currency(pair)
|
base_currency = self.exchange.get_pair_base_currency(pair)
|
||||||
open_date = datetime.now(timezone.utc)
|
open_date = datetime.now(UTC)
|
||||||
|
|
||||||
funding_fees = self.exchange.get_funding_fees(
|
funding_fees = self.exchange.get_funding_fees(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
@@ -1106,7 +1108,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
)(
|
)(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
trade=trade,
|
trade=trade,
|
||||||
current_time=datetime.now(timezone.utc),
|
current_time=datetime.now(UTC),
|
||||||
proposed_rate=enter_limit_requested,
|
proposed_rate=enter_limit_requested,
|
||||||
entry_tag=entry_tag,
|
entry_tag=entry_tag,
|
||||||
side=trade_side,
|
side=trade_side,
|
||||||
@@ -1124,7 +1126,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
else:
|
else:
|
||||||
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
|
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
current_time=datetime.now(timezone.utc),
|
current_time=datetime.now(UTC),
|
||||||
current_rate=enter_limit_requested,
|
current_rate=enter_limit_requested,
|
||||||
proposed_leverage=1.0,
|
proposed_leverage=1.0,
|
||||||
max_leverage=max_leverage,
|
max_leverage=max_leverage,
|
||||||
@@ -1157,7 +1159,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.strategy.custom_stake_amount, default_retval=stake_amount
|
self.strategy.custom_stake_amount, default_retval=stake_amount
|
||||||
)(
|
)(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
current_time=datetime.now(timezone.utc),
|
current_time=datetime.now(UTC),
|
||||||
current_rate=enter_limit_requested,
|
current_rate=enter_limit_requested,
|
||||||
proposed_stake=stake_amount,
|
proposed_stake=stake_amount,
|
||||||
min_stake=min_stake_amount,
|
min_stake=min_stake_amount,
|
||||||
@@ -1214,6 +1216,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"leverage": trade.leverage if trade.leverage else None,
|
"leverage": trade.leverage if trade.leverage else None,
|
||||||
"direction": "Short" if trade.is_short else "Long",
|
"direction": "Short" if trade.is_short else "Long",
|
||||||
"limit": open_rate, # Deprecated (?)
|
"limit": open_rate, # Deprecated (?)
|
||||||
|
"order_rate": open_rate,
|
||||||
"open_rate": open_rate,
|
"open_rate": open_rate,
|
||||||
"order_type": order_type or "unknown",
|
"order_type": order_type or "unknown",
|
||||||
"stake_amount": stake_amount,
|
"stake_amount": stake_amount,
|
||||||
@@ -1222,7 +1225,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"quote_currency": self.exchange.get_pair_quote_currency(trade.pair),
|
"quote_currency": self.exchange.get_pair_quote_currency(trade.pair),
|
||||||
"fiat_currency": self.config.get("fiat_display_currency", None),
|
"fiat_currency": self.config.get("fiat_display_currency", None),
|
||||||
"amount": order.safe_amount_after_fee if fill else (order.safe_amount or trade.amount),
|
"amount": order.safe_amount_after_fee if fill else (order.safe_amount or trade.amount),
|
||||||
"open_date": trade.open_date_utc or datetime.now(timezone.utc),
|
"open_date": trade.open_date_utc or datetime.now(UTC),
|
||||||
"current_rate": current_rate,
|
"current_rate": current_rate,
|
||||||
"sub_trade": sub_trade,
|
"sub_trade": sub_trade,
|
||||||
}
|
}
|
||||||
@@ -1250,6 +1253,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"leverage": trade.leverage,
|
"leverage": trade.leverage,
|
||||||
"direction": "Short" if trade.is_short else "Long",
|
"direction": "Short" if trade.is_short else "Long",
|
||||||
"limit": trade.open_rate,
|
"limit": trade.open_rate,
|
||||||
|
"order_rate": trade.open_rate,
|
||||||
"order_type": order_type,
|
"order_type": order_type,
|
||||||
"stake_amount": trade.stake_amount,
|
"stake_amount": trade.stake_amount,
|
||||||
"open_rate": trade.open_rate,
|
"open_rate": trade.open_rate,
|
||||||
@@ -1361,7 +1365,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
exits: list[ExitCheckTuple] = self.strategy.should_exit(
|
exits: list[ExitCheckTuple] = self.strategy.should_exit(
|
||||||
trade,
|
trade,
|
||||||
exit_rate,
|
exit_rate,
|
||||||
datetime.now(timezone.utc),
|
datetime.now(UTC),
|
||||||
enter=enter,
|
enter=enter,
|
||||||
exit_=exit_,
|
exit_=exit_,
|
||||||
force_stoploss=0,
|
force_stoploss=0,
|
||||||
@@ -1479,44 +1483,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: CcxtOrder) -> None:
|
|
||||||
"""
|
|
||||||
Check to see if stoploss on exchange should be updated
|
|
||||||
in case of trailing stoploss on exchange
|
|
||||||
:param trade: Corresponding Trade
|
|
||||||
:param order: Current on exchange stoploss order
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
stoploss_norm = self.exchange.price_to_precision(
|
|
||||||
trade.pair,
|
|
||||||
trade.stoploss_or_liquidation,
|
|
||||||
rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
|
|
||||||
# we check if the update is necessary
|
|
||||||
update_beat = self.strategy.order_types.get("stoploss_on_exchange_interval", 60)
|
|
||||||
upd_req = datetime.now(timezone.utc) - timedelta(seconds=update_beat)
|
|
||||||
if trade.stoploss_last_update_utc and upd_req >= trade.stoploss_last_update_utc:
|
|
||||||
# cancelling the current stoploss on exchange first
|
|
||||||
logger.info(
|
|
||||||
f"Cancelling current stoploss on exchange for pair {trade.pair} "
|
|
||||||
f"(orderid:{order['id']}) in order to add another one ..."
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cancel_stoploss_on_exchange(trade)
|
|
||||||
if not trade.is_open:
|
|
||||||
logger.warning(
|
|
||||||
f"Trade {trade} is closed, not creating trailing stoploss order."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create new stoploss order
|
|
||||||
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
|
|
||||||
logger.warning(
|
|
||||||
f"Could not create trailing stoploss order for pair {trade.pair}."
|
|
||||||
)
|
|
||||||
|
|
||||||
def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: list[CcxtOrder]):
|
def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: list[CcxtOrder]):
|
||||||
"""
|
"""
|
||||||
Perform required actions according to existing stoploss orders of trade
|
Perform required actions according to existing stoploss orders of trade
|
||||||
@@ -1558,6 +1524,44 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: CcxtOrder) -> None:
|
||||||
|
"""
|
||||||
|
Check to see if stoploss on exchange should be updated
|
||||||
|
in case of trailing stoploss on exchange
|
||||||
|
:param trade: Corresponding Trade
|
||||||
|
:param order: Current on exchange stoploss order
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
stoploss_norm = self.exchange.price_to_precision(
|
||||||
|
trade.pair,
|
||||||
|
trade.stoploss_or_liquidation,
|
||||||
|
rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
|
||||||
|
# we check if the update is necessary
|
||||||
|
update_beat = self.strategy.order_types.get("stoploss_on_exchange_interval", 60)
|
||||||
|
upd_req = datetime.now(UTC) - timedelta(seconds=update_beat)
|
||||||
|
if trade.stoploss_last_update_utc and upd_req >= trade.stoploss_last_update_utc:
|
||||||
|
# cancelling the current stoploss on exchange first
|
||||||
|
logger.info(
|
||||||
|
f"Cancelling current stoploss on exchange for pair {trade.pair} "
|
||||||
|
f"(orderid:{order['id']}) in order to add another one ..."
|
||||||
|
)
|
||||||
|
|
||||||
|
self.cancel_stoploss_on_exchange(trade)
|
||||||
|
if not trade.is_open:
|
||||||
|
logger.warning(
|
||||||
|
f"Trade {trade} is closed, not creating trailing stoploss order."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create new stoploss order
|
||||||
|
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
|
||||||
|
logger.warning(
|
||||||
|
f"Could not create trailing stoploss order for pair {trade.pair}."
|
||||||
|
)
|
||||||
|
|
||||||
def manage_open_orders(self) -> None:
|
def manage_open_orders(self) -> None:
|
||||||
"""
|
"""
|
||||||
Management of open orders on exchange. Unfilled orders might be cancelled if timeout
|
Management of open orders on exchange. Unfilled orders might be cancelled if timeout
|
||||||
@@ -1583,9 +1587,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if not_closed:
|
if not_closed:
|
||||||
if fully_cancelled or (
|
if fully_cancelled or (
|
||||||
open_order
|
open_order
|
||||||
and self.strategy.ft_check_timed_out(
|
and self.strategy.ft_check_timed_out(trade, open_order, datetime.now(UTC))
|
||||||
trade, open_order, datetime.now(timezone.utc)
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
self.handle_cancel_order(
|
self.handle_cancel_order(
|
||||||
order, open_order, trade, constants.CANCEL_REASON["TIMEOUT"]
|
order, open_order, trade, constants.CANCEL_REASON["TIMEOUT"]
|
||||||
@@ -1683,7 +1685,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade=trade,
|
trade=trade,
|
||||||
order=order_obj,
|
order=order_obj,
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
current_time=datetime.now(timezone.utc),
|
current_time=datetime.now(UTC),
|
||||||
proposed_rate=proposed_rate,
|
proposed_rate=proposed_rate,
|
||||||
current_order_rate=order_obj.safe_placement_price,
|
current_order_rate=order_obj.safe_placement_price,
|
||||||
entry_tag=trade.enter_tag,
|
entry_tag=trade.enter_tag,
|
||||||
@@ -2075,7 +2077,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
)(
|
)(
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
trade=trade,
|
trade=trade,
|
||||||
current_time=datetime.now(timezone.utc),
|
current_time=datetime.now(UTC),
|
||||||
proposed_rate=proposed_limit_rate,
|
proposed_rate=proposed_limit_rate,
|
||||||
current_profit=current_profit,
|
current_profit=current_profit,
|
||||||
exit_tag=exit_reason,
|
exit_tag=exit_reason,
|
||||||
@@ -2106,7 +2108,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
time_in_force=time_in_force,
|
time_in_force=time_in_force,
|
||||||
exit_reason=exit_reason,
|
exit_reason=exit_reason,
|
||||||
sell_reason=exit_reason, # sellreason -> compatibility
|
sell_reason=exit_reason, # sellreason -> compatibility
|
||||||
current_time=datetime.now(timezone.utc),
|
current_time=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
logger.info(f"User denied exit for {trade.pair}.")
|
logger.info(f"User denied exit for {trade.pair}.")
|
||||||
@@ -2202,7 +2204,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"enter_tag": trade.enter_tag,
|
"enter_tag": trade.enter_tag,
|
||||||
"exit_reason": trade.exit_reason,
|
"exit_reason": trade.exit_reason,
|
||||||
"open_date": trade.open_date_utc,
|
"open_date": trade.open_date_utc,
|
||||||
"close_date": trade.close_date_utc or datetime.now(timezone.utc),
|
"close_date": trade.close_date_utc or datetime.now(UTC),
|
||||||
"stake_amount": trade.stake_amount,
|
"stake_amount": trade.stake_amount,
|
||||||
"stake_currency": self.config["stake_currency"],
|
"stake_currency": self.config["stake_currency"],
|
||||||
"base_currency": self.exchange.get_pair_base_currency(trade.pair),
|
"base_currency": self.exchange.get_pair_base_currency(trade.pair),
|
||||||
@@ -2247,6 +2249,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"direction": "Short" if trade.is_short else "Long",
|
"direction": "Short" if trade.is_short else "Long",
|
||||||
"gain": gain,
|
"gain": gain,
|
||||||
"limit": profit_rate or 0,
|
"limit": profit_rate or 0,
|
||||||
|
"order_rate": profit_rate or 0,
|
||||||
"order_type": order_type,
|
"order_type": order_type,
|
||||||
"amount": order.safe_amount_after_fee,
|
"amount": order.safe_amount_after_fee,
|
||||||
"open_rate": trade.open_rate,
|
"open_rate": trade.open_rate,
|
||||||
@@ -2257,7 +2260,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"enter_tag": trade.enter_tag,
|
"enter_tag": trade.enter_tag,
|
||||||
"exit_reason": trade.exit_reason,
|
"exit_reason": trade.exit_reason,
|
||||||
"open_date": trade.open_date,
|
"open_date": trade.open_date,
|
||||||
"close_date": trade.close_date or datetime.now(timezone.utc),
|
"close_date": trade.close_date or datetime.now(UTC),
|
||||||
"stake_currency": self.config["stake_currency"],
|
"stake_currency": self.config["stake_currency"],
|
||||||
"base_currency": self.exchange.get_pair_base_currency(trade.pair),
|
"base_currency": self.exchange.get_pair_base_currency(trade.pair),
|
||||||
"quote_currency": self.exchange.get_pair_quote_currency(trade.pair),
|
"quote_currency": self.exchange.get_pair_quote_currency(trade.pair),
|
||||||
@@ -2337,8 +2340,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
def _update_trade_after_fill(self, trade: Trade, order: Order, send_msg: bool) -> Trade:
|
def _update_trade_after_fill(self, trade: Trade, order: Order, send_msg: bool) -> Trade:
|
||||||
if order.status in constants.NON_OPEN_EXCHANGE_STATES:
|
if order.status in constants.NON_OPEN_EXCHANGE_STATES:
|
||||||
strategy_safe_wrapper(self.strategy.order_filled, default_retval=None)(
|
strategy_safe_wrapper(self.strategy.order_filled, supress_error=True)(
|
||||||
pair=trade.pair, trade=trade, order=order, current_time=datetime.now(timezone.utc)
|
pair=trade.pair, trade=trade, order=order, current_time=datetime.now(UTC)
|
||||||
)
|
)
|
||||||
# If a entry order was closed, force update on stoploss on exchange
|
# If a entry order was closed, force update on stoploss on exchange
|
||||||
if order.ft_order_side == trade.entry_side:
|
if order.ft_order_side == trade.entry_side:
|
||||||
@@ -2365,14 +2368,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
stake_currency=self.config["stake_currency"],
|
stake_currency=self.config["stake_currency"],
|
||||||
dry_run=self.config["dry_run"],
|
dry_run=self.config["dry_run"],
|
||||||
)
|
)
|
||||||
if self.strategy.use_custom_stoploss:
|
if self.strategy.use_custom_stoploss and trade.is_open:
|
||||||
current_rate = self.exchange.get_rate(
|
current_rate = self.exchange.get_rate(
|
||||||
trade.pair, side="exit", is_short=trade.is_short, refresh=True
|
trade.pair, side="exit", is_short=trade.is_short, refresh=True
|
||||||
)
|
)
|
||||||
profit = trade.calc_profit_ratio(current_rate)
|
profit = trade.calc_profit_ratio(current_rate)
|
||||||
self.strategy.ft_stoploss_adjust(
|
self.strategy.ft_stoploss_adjust(
|
||||||
current_rate, trade, datetime.now(timezone.utc), profit, 0, after_fill=True
|
current_rate, trade, datetime.now(UTC), profit, 0, after_fill=True
|
||||||
)
|
)
|
||||||
# Updating wallets when order is closed
|
# Updating wallets when order is closed
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
return trade
|
return trade
|
||||||
@@ -2397,7 +2400,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
def handle_protections(self, pair: str, side: LongShort) -> None:
|
def handle_protections(self, pair: str, side: LongShort) -> None:
|
||||||
# Lock pair for one candle to prevent immediate re-entries
|
# Lock pair for one candle to prevent immediate re-entries
|
||||||
self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason="Auto lock", side=side)
|
self.strategy.lock_pair(pair, datetime.now(UTC), reason="Auto lock", side=side)
|
||||||
prot_trig = self.protections.stop_per_pair(pair, side=side)
|
prot_trig = self.protections.stop_per_pair(pair, side=side)
|
||||||
if prot_trig:
|
if prot_trig:
|
||||||
msg: RPCProtectionMsg = {
|
msg: RPCProtectionMsg = {
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ from freqtrade.ft_types.backtest_result_type import (
|
|||||||
get_BacktestResultType_default,
|
get_BacktestResultType_default,
|
||||||
)
|
)
|
||||||
from freqtrade.ft_types.plot_annotation_type import AnnotationType
|
from freqtrade.ft_types.plot_annotation_type import AnnotationType
|
||||||
from freqtrade.ft_types.valid_exchanges_type import ValidExchangesType
|
from freqtrade.ft_types.valid_exchanges_type import TradeModeType, ValidExchangesType
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal, Required
|
||||||
|
|
||||||
from pydantic import TypeAdapter
|
from pydantic import TypeAdapter
|
||||||
from typing_extensions import Required, TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
|
|
||||||
class AnnotationType(TypedDict, total=False):
|
class AnnotationType(TypedDict, total=False):
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ def setup_logging_pre() -> None:
|
|||||||
FT_LOGGING_CONFIG = {
|
FT_LOGGING_CONFIG = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
# "incremental": True,
|
# "incremental": True,
|
||||||
# "disable_existing_loggers": False,
|
"disable_existing_loggers": False,
|
||||||
"formatters": {
|
"formatters": {
|
||||||
"basic": {"format": "%(message)s"},
|
"basic": {"format": "%(message)s"},
|
||||||
"standard": {
|
"standard": {
|
||||||
@@ -223,7 +223,7 @@ def setup_logging(config: Config) -> None:
|
|||||||
logger.info("Enabling colorized output.")
|
logger.info("Enabling colorized output.")
|
||||||
error_console._color_system = error_console._detect_color_system()
|
error_console._color_system = error_console._detect_color_system()
|
||||||
|
|
||||||
logging.info("Logfile configured")
|
logger.info("Logfile configured")
|
||||||
|
|
||||||
# Set verbosity levels
|
# Set verbosity levels
|
||||||
logging.root.setLevel(logging.INFO if verbosity < 1 else logging.DEBUG)
|
logging.root.setLevel(logging.INFO if verbosity < 1 else logging.DEBUG)
|
||||||
|
|||||||
+2
-2
@@ -10,8 +10,8 @@ from typing import Any
|
|||||||
|
|
||||||
|
|
||||||
# check min. python version
|
# check min. python version
|
||||||
if sys.version_info < (3, 10): # pragma: no cover # noqa: UP036
|
if sys.version_info < (3, 11): # pragma: no cover # noqa: UP036
|
||||||
sys.exit("Freqtrade requires Python version >= 3.10")
|
sys.exit("Freqtrade requires Python version >= 3.11")
|
||||||
|
|
||||||
from freqtrade import __version__
|
from freqtrade import __version__
|
||||||
from freqtrade.commands import Arguments
|
from freqtrade.commands import Arguments
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@@ -38,7 +38,7 @@ class BaseAnalysis:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def dt_to_timestamp(dt: datetime):
|
def dt_to_timestamp(dt: datetime):
|
||||||
timestamp = int(dt.replace(tzinfo=timezone.utc).timestamp())
|
timestamp = int(dt.replace(tzinfo=UTC).timestamp())
|
||||||
return timestamp
|
return timestamp
|
||||||
|
|
||||||
def fill_full_varholder(self):
|
def fill_full_varholder(self):
|
||||||
@@ -48,12 +48,12 @@ class BaseAnalysis:
|
|||||||
parsed_timerange = TimeRange.parse_timerange(self.local_config["timerange"])
|
parsed_timerange = TimeRange.parse_timerange(self.local_config["timerange"])
|
||||||
|
|
||||||
if parsed_timerange.startdt is None:
|
if parsed_timerange.startdt is None:
|
||||||
self.full_varHolder.from_dt = datetime.fromtimestamp(0, tz=timezone.utc)
|
self.full_varHolder.from_dt = datetime.fromtimestamp(0, tz=UTC)
|
||||||
else:
|
else:
|
||||||
self.full_varHolder.from_dt = parsed_timerange.startdt
|
self.full_varHolder.from_dt = parsed_timerange.startdt
|
||||||
|
|
||||||
if parsed_timerange.stopdt is None:
|
if parsed_timerange.stopdt is None:
|
||||||
self.full_varHolder.to_dt = datetime.now(timezone.utc)
|
self.full_varHolder.to_dt = datetime.now(UTC)
|
||||||
else:
|
else:
|
||||||
self.full_varHolder.to_dt = parsed_timerange.stopdt
|
self.full_varHolder.to_dt = parsed_timerange.stopdt
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user