Compare commits
479 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da52ef8729 | |||
| 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 | |||
| cc807d1cf9 | |||
| 15c836c566 | |||
| 5f907a4b1a | |||
| a464a2fdbc | |||
| fa3d95fd89 | |||
| ba8778b181 | |||
| 6421f767c6 | |||
| f18c7f0cad | |||
| e63017a2b9 | |||
| 049cc10169 | |||
| 595db54ac7 | |||
| bfa9c818e6 | |||
| 30afedd4f8 | |||
| 6049ac1561 | |||
| 25c24ddc9c | |||
| 03df35b49b | |||
| 3b441cd08f | |||
| 0f67d32b75 | |||
| 820a83c96f | |||
| fae30c6ff9 | |||
| 3e4ffb9c1d | |||
| f6d10dad6a | |||
| 79b530d98e | |||
| 3b8e521b81 | |||
| 7e228ba7ab | |||
| 6050d09ed8 | |||
| b7f158a79e | |||
| bc13d1636c | |||
| 021fdcab43 | |||
| d206b601b3 | |||
| 949c7318f4 | |||
| 3666362479 | |||
| f27540d221 | |||
| 31dc0fe327 | |||
| 1125c60f07 | |||
| 9f5fd574b4 | |||
| 74eb0d9f88 | |||
| 52c78966ef | |||
| 1641f56956 | |||
| d27981f8cf | |||
| 6031ccabac | |||
| c64b6b0d69 | |||
| 4203693428 | |||
| c76c26cc34 | |||
| 1c87748b99 | |||
| 4ae80423e0 | |||
| 4e6ae88557 | |||
| 102c33d68b | |||
| 12a72fa161 | |||
| 3a5b555f1e | |||
| 8f2ee28395 | |||
| e57d0e3bc4 | |||
| f5205da459 | |||
| bd773b9baf | |||
| 7b4a6a48ee | |||
| 24a3a29360 | |||
| 6a279043fa | |||
| 60764500db | |||
| ba5a64aa34 | |||
| f40576091a | |||
| de3eedb39e | |||
| fded8bf43e | |||
| 5a45b4a3e1 | |||
| 0b3ea4d0ed | |||
| 822dce09b7 | |||
| 2ebc5374f4 | |||
| 3465f35d75 | |||
| 2165c3e542 | |||
| 5e059660d4 | |||
| 5ca69687dc | |||
| d042085b24 | |||
| effd989796 | |||
| ca85deafb8 | |||
| d46cc552be | |||
| 1dc2bcb47e | |||
| d6bf20b7b4 | |||
| 2d842ea129 | |||
| 76d5423eee | |||
| 489b7e6d18 | |||
| 18dfb0fe39 | |||
| 63efed736c | |||
| 4c568e40c2 | |||
| b54d231b79 | |||
| 58f361f8b3 | |||
| f78d383448 | |||
| 1be9a79e96 | |||
| d9e98bb687 | |||
| 7fd7061304 | |||
| db50d2b85d | |||
| d2a1e52d79 | |||
| 70e78e6521 | |||
| 913148bf1d | |||
| 0ecb3e45f6 | |||
| 74f870c435 | |||
| 8f62462e15 | |||
| 38183fe952 | |||
| 215b111cff | |||
| 4ea9d70743 | |||
| 0893350285 | |||
| 27bd34c3be | |||
| ebf6ca1a5f | |||
| a91ed0b4d9 | |||
| 097b27208d | |||
| 136623b3f5 | |||
| 02648e29c4 | |||
| beb6d41360 | |||
| a1207eca56 | |||
| 1a247a1035 | |||
| 4e0203d4af | |||
| 332518c1a0 | |||
| 15353f44f0 | |||
| 093cdae39c | |||
| 5dd5b95d6a | |||
| 20e0b542e3 | |||
| 1d24f9047c | |||
| f2c37e2575 | |||
| 95fbe04225 | |||
| 43c5b8d0ed | |||
| 301d85ef4f | |||
| 48f3d612e9 | |||
| c53c70d597 | |||
| aa89ce80ab | |||
| 971dfef0ee | |||
| 5d8a3cdb31 | |||
| 5246eecaf6 | |||
| 18b6f374d6 | |||
| 95c6997209 | |||
| ca79b84e4a | |||
| 3670df8771 | |||
| ff06d58ace | |||
| 1b4592a2f3 | |||
| 5138e97b70 | |||
| 340cad3707 | |||
| ca32cb9e61 | |||
| c911b7fa70 | |||
| 9323b6b946 | |||
| 92ef1ef761 | |||
| e38504b9db | |||
| e437847916 | |||
| a3a30f9046 | |||
| df507e2335 | |||
| 2f1f60a8fc | |||
| 1c1a165ba4 | |||
| 00fe02c854 | |||
| 6ff26b075e | |||
| 03a46ec784 | |||
| ec31764aed | |||
| 1ba411cccf | |||
| 7957231ce4 | |||
| 6b88c49e0a | |||
| 4fe99e0333 | |||
| 9a38dfcc26 | |||
| 3ecbfeb1d5 | |||
| 02229c959f | |||
| 2a2f205fbe | |||
| c415dab846 | |||
| 5b47960567 | |||
| 3f9749b03b | |||
| 528b11af07 | |||
| 0a31ff3747 | |||
| e1ee566634 | |||
| 1cff4d13de | |||
| 22b1bc7210 | |||
| c741c168e8 | |||
| c997f02085 | |||
| ead6d89e35 | |||
| 985c15bdd9 | |||
| 7fb7c43e45 | |||
| 0e68de0c6d | |||
| ba5f771e65 | |||
| e0f147e076 | |||
| b874ceaf2f | |||
| 11f339f802 | |||
| 9d46fd21dc | |||
| 286f385f99 | |||
| 69cca13d15 | |||
| 37d03a5b44 | |||
| 0e1d3e279c | |||
| ae671c736e | |||
| 28064d013c | |||
| 5b9c03e7e2 | |||
| f2569e36e8 | |||
| bf2092977c | |||
| 613e31a9a5 | |||
| 326a4f17d2 | |||
| a8d617d747 | |||
| ba9f795df5 | |||
| dbccc69f05 | |||
| db80318946 | |||
| 5698793dcc | |||
| bf96be2b71 | |||
| 2af454333b | |||
| cb869a2e4d | |||
| afc5205bea | |||
| 842f3a2a36 | |||
| 582871b0d5 | |||
| c6c2934741 | |||
| d3b5be6cb6 | |||
| 473a15c4ef | |||
| 2c4452453a | |||
| da6affcd6a | |||
| 3ebbe2b56c | |||
| 8f13a5db29 | |||
| 3155dfc14c | |||
| cc30e71ab2 | |||
| f5cb486f5e | |||
| 42c8795141 | |||
| f878d051df | |||
| aefa7e401c | |||
| dc06510d4a | |||
| 7740b61f02 | |||
| 950f5a722e | |||
| 9b56245b1d | |||
| b05fb57fb7 | |||
| 1a4db77131 | |||
| 71920cefb1 | |||
| b5cbbd7c30 | |||
| f75a0817a6 | |||
| d12132ae7a | |||
| a81bec0da2 | |||
| cc41cb831f | |||
| 2b70ec896d | |||
| 470dbb6060 | |||
| afc61ca788 | |||
| ba62e5e007 | |||
| 9af401b091 | |||
| 1fdbace02d | |||
| cfeda4493b | |||
| 0de48d90f8 | |||
| c4eb26be9d | |||
| 04a8539f10 | |||
| d600461a84 | |||
| ce46cd7343 | |||
| d1cca4ac51 | |||
| eb5d5e0d3f | |||
| 77dcc5354c | |||
| 3ac4a872b1 | |||
| b8b94d58c4 | |||
| 33b5482065 | |||
| 43fea43363 | |||
| 168593d7af | |||
| 29ae71333a | |||
| 7a84d1ecba | |||
| bc78e2e1b9 | |||
| 3fb6bc888b | |||
| 0228ec796b | |||
| 69de7b4045 | |||
| 825c059c2e | |||
| 8e8f4dbdd3 | |||
| 865c253922 | |||
| cd6e06c86e | |||
| 422f225e8e | |||
| 9921c54882 |
@@ -1,5 +1,10 @@
|
||||
<!-- 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)
|
||||
|
||||
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
|
||||
|
||||
|
||||
+24
-106
@@ -25,7 +25,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ "ubuntu-22.04", "ubuntu-24.04" ]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
python-version: ["3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -90,6 +90,7 @@ jobs:
|
||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||
run: |
|
||||
# Allow failure for coveralls
|
||||
uv pip install coveralls
|
||||
coveralls || true
|
||||
|
||||
- name: Run json schema extract
|
||||
@@ -103,6 +104,8 @@ jobs:
|
||||
python build_helpers/create_command_partials.py
|
||||
|
||||
- name: Check for repository changes
|
||||
# TODO: python 3.13 slightly changed the output of argparse.
|
||||
if: (matrix.python-version != '3.13')
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "Repository is dirty, changes detected:"
|
||||
@@ -145,7 +148,7 @@ jobs:
|
||||
mypy freqtrade scripts tests
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: error
|
||||
@@ -156,8 +159,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ "macos-13", "macos-14", "macos-15" ]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
os: [ "macos-14", "macos-15" ]
|
||||
python-version: ["3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -171,7 +174,7 @@ jobs:
|
||||
check-latest: true
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -272,7 +275,7 @@ jobs:
|
||||
mypy freqtrade scripts
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: info
|
||||
@@ -285,7 +288,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ windows-latest ]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
python-version: ["3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -298,7 +301,7 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -366,7 +369,7 @@ jobs:
|
||||
shell: powershell
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: error
|
||||
@@ -424,7 +427,7 @@ jobs:
|
||||
mkdocs build
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: error
|
||||
@@ -446,7 +449,7 @@ jobs:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -512,7 +515,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.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)
|
||||
with:
|
||||
severity: info
|
||||
@@ -616,100 +619,15 @@ jobs:
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
|
||||
|
||||
deploy-docker:
|
||||
docker-build:
|
||||
name: "Docker Build and Deploy"
|
||||
needs: [ build-linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract-branch
|
||||
run: |
|
||||
echo "GITHUB_REF='${GITHUB_REF}'"
|
||||
echo "branch=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Dockerhub login
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
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
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 #v3.10.0
|
||||
|
||||
- name: Available platforms
|
||||
run: echo ${PLATFORMS}
|
||||
env:
|
||||
PLATFORMS: ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Build and test and push docker images
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.extract-branch.outputs.branch }}
|
||||
run: |
|
||||
build_helpers/publish_docker_multi.sh
|
||||
|
||||
deploy-arm:
|
||||
name: "Deploy Docker"
|
||||
uses: ./.github/workflows/docker-build.yml
|
||||
permissions:
|
||||
packages: write
|
||||
needs: [ deploy-docker ]
|
||||
# Only run on 64bit machines
|
||||
runs-on: [self-hosted, linux, ARM64]
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract-branch
|
||||
run: |
|
||||
echo "GITHUB_REF='${GITHUB_REF}'"
|
||||
echo "branch=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Dockerhub login
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
|
||||
|
||||
- name: Build and test and push docker images
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.extract-branch.outputs.branch }}
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
build_helpers/publish_docker_arm64.sh
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && (github.event_name != 'schedule')
|
||||
with:
|
||||
severity: info
|
||||
details: Deploy Succeeded!
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
contents: read
|
||||
secrets:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
name: Docker Build and Deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
DOCKER_PASSWORD:
|
||||
required: true
|
||||
DOCKER_USERNAME:
|
||||
required: true
|
||||
DISCORD_WEBHOOK:
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch_name:
|
||||
description: 'Branch name to build Docker images for'
|
||||
required: false
|
||||
default: 'develop'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy-docker:
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract-branch
|
||||
env:
|
||||
BRANCH_NAME_INPUT: ${{ github.event.inputs.branch_name }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
BRANCH_NAME="${BRANCH_NAME_INPUT}"
|
||||
else
|
||||
BRANCH_NAME="${GITHUB_REF##*/}"
|
||||
fi
|
||||
echo "GITHUB_REF='${GITHUB_REF}'"
|
||||
echo "branch=${BRANCH_NAME}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Dockerhub login
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
|
||||
|
||||
- name: Available platforms
|
||||
run: echo ${PLATFORMS}
|
||||
env:
|
||||
PLATFORMS: ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Build and test and push docker images
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.extract-branch.outputs.branch }}
|
||||
run: |
|
||||
build_helpers/publish_docker_multi.sh
|
||||
|
||||
deploy-arm:
|
||||
name: "Deploy Docker"
|
||||
permissions:
|
||||
packages: write
|
||||
needs: [ deploy-docker ]
|
||||
# Only run on 64bit machines
|
||||
runs-on: [self-hosted, linux, ARM64]
|
||||
if: github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract-branch
|
||||
env:
|
||||
BRANCH_NAME_INPUT: ${{ github.event.inputs.branch_name }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
BRANCH_NAME="${BRANCH_NAME_INPUT}"
|
||||
else
|
||||
BRANCH_NAME="${GITHUB_REF##*/}"
|
||||
fi
|
||||
echo "GITHUB_REF='${GITHUB_REF}'"
|
||||
echo "branch=${BRANCH_NAME}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Dockerhub login
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
|
||||
|
||||
- name: Build and test and push docker images
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.extract-branch.outputs.branch }}
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
build_helpers/publish_docker_arm64.sh
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
||||
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && (github.event_name != 'schedule')
|
||||
with:
|
||||
severity: info
|
||||
details: Deploy Succeeded!
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
@@ -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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@f52a838cfabf134edcbaa7c8b3677dde20045018 # v0.1.1
|
||||
@@ -14,23 +14,24 @@ repos:
|
||||
additional_dependencies: ["python-rapidjson", "jsonschema"]
|
||||
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: "7.2.0"
|
||||
rev: "7.3.0"
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [Flake8-pyproject]
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.15.0"
|
||||
rev: "v1.17.0"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==6.0.0.20250525
|
||||
- types-cachetools==6.1.0.20250717
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.32.0.20250515
|
||||
- types-requests==2.32.4.20250611
|
||||
- types-tabulate==0.9.0.20241207
|
||||
- types-python-dateutil==2.9.0.20250516
|
||||
- types-python-dateutil==2.9.0.20250708
|
||||
- scipy-stubs==1.16.0.2
|
||||
- SQLAlchemy==2.0.41
|
||||
# stages: [push]
|
||||
|
||||
@@ -43,7 +44,7 @@ repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.11.11'
|
||||
rev: 'v0.12.5'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
@@ -69,7 +70,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/stefmolin/exif-stripper
|
||||
rev: 0.6.2
|
||||
rev: 1.1.0
|
||||
hooks:
|
||||
- id: strip-exif
|
||||
|
||||
@@ -82,6 +83,6 @@ repos:
|
||||
|
||||
# Ensure github actions remain safe
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.8.0
|
||||
rev: v1.11.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
+10
-10
@@ -1,10 +1,10 @@
|
||||
FROM python:3.12.10-slim-bookworm as base
|
||||
FROM python:3.13.5-slim-bookworm AS base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONFAULTHANDLER 1
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONFAULTHANDLER=1
|
||||
ENV PATH=/home/ftuser/.local/bin:$PATH
|
||||
ENV FT_APP_ENV="docker"
|
||||
|
||||
@@ -21,7 +21,7 @@ RUN mkdir /freqtrade \
|
||||
WORKDIR /freqtrade
|
||||
|
||||
# Install dependencies
|
||||
FROM base as python-deps
|
||||
FROM base AS python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
@@ -30,18 +30,18 @@ RUN apt-get update \
|
||||
# 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
|
||||
ENV LD_LIBRARY_PATH=/usr/local/lib
|
||||
|
||||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
|
||||
USER ftuser
|
||||
RUN pip install --user --no-cache-dir "numpy<2.0" \
|
||||
RUN pip install --user --no-cache-dir "numpy<3.0" \
|
||||
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
|
||||
@@ -64,13 +64,12 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
||||
|
||||
## 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] **Dry-run**: Run the bot without paying money.
|
||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
||||
- [X] **Adaptive prediction modeling**: Build a smart strategy with FreqAI that self-trains to the market via adaptive machine learning methods. [Learn more](https://www.freqtrade.io/en/stable/freqai/)
|
||||
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/).
|
||||
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
|
||||
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
||||
- [x] **Builtin WebUI**: Builtin web UI to manage your bot.
|
||||
@@ -112,7 +111,6 @@ positional arguments:
|
||||
backtesting-show Show past Backtest results
|
||||
backtesting-analysis
|
||||
Backtest Analysis module.
|
||||
edge Edge module.
|
||||
hyperopt Hyperopt module.
|
||||
hyperopt-list List Hyperopt results
|
||||
hyperopt-show Show details of Hyperopt results
|
||||
@@ -148,6 +146,8 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
|
||||
- `/stopentry`: Stop entering new 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_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`).
|
||||
- `/fx <trade_id>|all`: Alias to `/forceexit`
|
||||
- `/performance`: Show performance of each finished trade grouped by pair
|
||||
@@ -156,6 +156,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
|
||||
- `/help`: Show help message.
|
||||
- `/version`: Show version.
|
||||
|
||||
|
||||
## Development branches
|
||||
|
||||
The project is currently setup in two main branches:
|
||||
@@ -221,7 +222,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||
|
||||
### 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/)
|
||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
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<2"
|
||||
pip install --only-binary ta-lib --find-links=build_helpers\ ta-lib
|
||||
pip install -U wheel "numpy<3.0"
|
||||
pip install --only-binary ta-lib --find-links=build_helpers\ "ta-lib<0.6.0"
|
||||
|
||||
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:
|
||||
requirements.extend(rfile.readlines())
|
||||
|
||||
# Extract types only
|
||||
type_reqs = [
|
||||
r.strip("\n") for r in requirements if r.startswith("types-") or r.startswith("SQLAlchemy")
|
||||
]
|
||||
# Extract relevant types only
|
||||
supported = ("types-", "SQLAlchemy", "scipy-stubs")
|
||||
|
||||
# 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:
|
||||
f = yaml.load(file, Loader=yaml.SafeLoader)
|
||||
|
||||
BIN
Binary file not shown.
@@ -538,10 +538,6 @@
|
||||
"description": "Exchange configuration.",
|
||||
"$ref": "#/definitions/exchange"
|
||||
},
|
||||
"edge": {
|
||||
"description": "Edge configuration.",
|
||||
"$ref": "#/definitions/edge"
|
||||
},
|
||||
"log_config": {
|
||||
"description": "Logging configuration.",
|
||||
"$ref": "#/definitions/logging"
|
||||
@@ -1247,7 +1243,11 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ccxt_async_config": {
|
||||
"description": "CCXT asynchronous configuration settings.",
|
||||
"description": "CCXT asynchronous configuration settings.Usually ccxt_config should be used instead.",
|
||||
"type": "object"
|
||||
},
|
||||
"ccxt_sync_config": {
|
||||
"description": "CCXT synchronous configuration settings. Usually ccxt_config should be used instead.",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
@@ -1255,52 +1255,6 @@
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"edge": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"process_throttle_secs": {
|
||||
"type": "integer",
|
||||
"minimum": 600
|
||||
},
|
||||
"calculate_since_number_of_days": {
|
||||
"type": "integer"
|
||||
},
|
||||
"allowed_risk": {
|
||||
"type": "number"
|
||||
},
|
||||
"stoploss_range_min": {
|
||||
"type": "number"
|
||||
},
|
||||
"stoploss_range_max": {
|
||||
"type": "number"
|
||||
},
|
||||
"stoploss_range_step": {
|
||||
"type": "number"
|
||||
},
|
||||
"minimum_winrate": {
|
||||
"type": "number"
|
||||
},
|
||||
"minimum_expectancy": {
|
||||
"type": "number"
|
||||
},
|
||||
"min_trade_number": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_trade_duration_minute": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remove_pumps": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"process_throttle_secs",
|
||||
"allowed_risk"
|
||||
]
|
||||
},
|
||||
"logging": {
|
||||
"type": "object",
|
||||
"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.
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 3,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": 0.05,
|
||||
"stake_amount": 30,
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"timeframe": "5m",
|
||||
|
||||
@@ -121,20 +121,6 @@
|
||||
"outdated_offset": 5,
|
||||
"markets_refresh_interval": 60
|
||||
},
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
"process_throttle_secs": 3600,
|
||||
"calculate_since_number_of_days": 7,
|
||||
"allowed_risk": 0.01,
|
||||
"stoploss_range_min": -0.01,
|
||||
"stoploss_range_max": -0.1,
|
||||
"stoploss_range_step": -0.01,
|
||||
"minimum_winrate": 0.60,
|
||||
"minimum_expectancy": 0.20,
|
||||
"min_trade_number": 10,
|
||||
"max_trade_duration_minute": 1440,
|
||||
"remove_pumps": false
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "your_telegram_token",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM python:3.11.12-slim-bookworm as base
|
||||
FROM python:3.11.13-slim-bookworm AS base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONFAULTHANDLER 1
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONFAULTHANDLER=1
|
||||
ENV PATH=/home/ftuser/.local/bin:$PATH
|
||||
ENV FT_APP_ENV="docker"
|
||||
|
||||
@@ -22,7 +22,7 @@ RUN mkdir /freqtrade \
|
||||
WORKDIR /freqtrade
|
||||
|
||||
# Install dependencies
|
||||
FROM base as python-deps
|
||||
FROM base AS python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
@@ -34,14 +34,14 @@ COPY build_helpers/* /tmp/
|
||||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
||||
USER ftuser
|
||||
RUN pip install --user --no-cache-dir "numpy<2" \
|
||||
RUN pip install --user --no-cache-dir "numpy<3.0" \
|
||||
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib \
|
||||
&& pip install --user --no-cache-dir -r requirements.txt
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
|
||||
+10
-1
@@ -5,6 +5,8 @@ This page explains how to validate your strategy performance by using Backtestin
|
||||
Backtesting requires historic data to be available.
|
||||
To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation.
|
||||
|
||||
Backtesting is also available in [webserver mode](freq-ui.md#backtesting), which allows you to run backtests via the web interface.
|
||||
|
||||
## Backtesting command reference
|
||||
|
||||
--8<-- "commands/backtesting.md"
|
||||
@@ -319,6 +321,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
| SQN | 2.45 |
|
||||
| Profit factor | 1.11 |
|
||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||
| Avg. daily profit | 0.0001 BTC |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
| Total trade volume | 0.429 BTC |
|
||||
| | |
|
||||
@@ -372,9 +375,11 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
- `Calmar`: Annualized Calmar ratio.
|
||||
- `SQN`: System Quality Number (SQN) - by Van Tharp.
|
||||
- `Profit factor`: profit / loss.
|
||||
- `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.
|
||||
- `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 %`.
|
||||
- `Best Pair` / `Worst Pair`: Best and worst performing pair (based on absolute profit), and it's corresponding `Tot Profit %`.
|
||||
- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade.
|
||||
- `Best day` / `Worst day`: Best and worst day based on daily profit.
|
||||
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
|
||||
@@ -435,6 +440,10 @@ To save time, by default backtest will reuse a cached result from within the las
|
||||
To further analyze your backtest results, freqtrade will export the trades to file by default.
|
||||
You can then load the trades to perform further analysis as shown in the [data analysis](strategy_analysis_example.md#load-backtest-results-to-pandas-dataframe) backtesting section.
|
||||
|
||||
Also, you can use freqtrade in [webserver mode](freq-ui.md#backtesting) to visualize the backtest results in a web interface.
|
||||
This mode also allows you to load existing backtest results, so you can analyze them without running the backtest again.
|
||||
For this mode - `--notes "<notes>"` can be used to add notes to the backtest results, which will be shown in the web interface.
|
||||
|
||||
### Backtest output file
|
||||
|
||||
The output file freqtrade produces is a zip file containing the following files:
|
||||
|
||||
@@ -17,7 +17,7 @@ usage: freqtrade backtesting [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[--export-filename PATH]
|
||||
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
||||
[--cache {none,day,week,month}]
|
||||
[--freqai-backtest-live-models]
|
||||
[--freqai-backtest-live-models] [--notes TEXT]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -73,6 +73,7 @@ options:
|
||||
age (default: day).
|
||||
--freqai-backtest-live-models
|
||||
Run backtest with ready models.
|
||||
--notes TEXT Add notes to the backtest results.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
||||
@@ -7,7 +7,6 @@ usage: freqtrade edge [-h] [-v] [--no-color] [--logfile FILE] [-V] [-c PATH]
|
||||
[--data-format-ohlcv {json,jsongz,feather,parquet}]
|
||||
[--max-open-trades INT] [--stake-amount STAKE_AMOUNT]
|
||||
[--fee FLOAT] [-p PAIRS [PAIRS ...]]
|
||||
[--stoplosses STOPLOSS_RANGE]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -29,11 +28,6 @@ options:
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Limit command to these pairs. Pairs are space-
|
||||
separated.
|
||||
--stoplosses STOPLOSS_RANGE
|
||||
Defines a range of stoploss values against which edge
|
||||
will assess the strategy. The format is "min,max,step"
|
||||
(without any space). Example:
|
||||
`--stoplosses=-0.01,-0.1,-0.001`
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
||||
@@ -22,7 +22,7 @@ positional arguments:
|
||||
backtesting-show Show past Backtest results
|
||||
backtesting-analysis
|
||||
Backtest Analysis module.
|
||||
edge Edge module.
|
||||
edge Edge module. No longer part of Freqtrade
|
||||
hyperopt Hyperopt module.
|
||||
hyperopt-list List Hyperopt results
|
||||
hyperopt-show Show details of Hyperopt results
|
||||
|
||||
@@ -234,7 +234,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `exchange.only_from_ccxt` | Prevent data-download from data.binance.vision. Leaving this as false can greatly speed up downloads, but may be problematic if the site is not available.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| | **Plugins**
|
||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
|
||||
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
|
||||
| | **Telegram**
|
||||
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
|
||||
|
||||
@@ -93,3 +93,8 @@ Please use the [`convert-data` subcommand](data-download.md#sub-command-convert-
|
||||
|
||||
Configuring syslog and journald via `--logfile systemd` and `--logfile journald` respectively has been deprecated in 2025.3.
|
||||
Please use configuration based [log setup](advanced-setup.md#advanced-logging) instead.
|
||||
|
||||
## Removal of the edge module
|
||||
|
||||
The edge module has been deprecated in 2023.9 and removed in 2025.6.
|
||||
All functionalities of edge have been removed, and having edge configured will result in an error.
|
||||
|
||||
@@ -304,6 +304,13 @@ The `IProtection` parent class provides a helper method for this in `calculate_l
|
||||
|
||||
Most exchanges supported by CCXT should work out of the box.
|
||||
|
||||
If you need to implement a specific exchange class, these are found in the `freqtrade/exchange` source folder. You'll also need to add the import to `freqtrade/exchange/__init__.py` to make the loading logic aware of the new exchange.
|
||||
We recommend looking at existing exchange implementations to get an idea of what might be required.
|
||||
|
||||
!!! Warning
|
||||
Implementing and testing an exchange can be a lot of trial and error, so please bear this in mind.
|
||||
You should also have some development experience, as this is not a beginner task.
|
||||
|
||||
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `tests/exchange_online/conftest.py` and run these tests with `pytest --longrun tests/exchange_online/test_ccxt_compat.py`.
|
||||
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
|
||||
|
||||
|
||||
-300
@@ -1,300 +0,0 @@
|
||||
# Edge positioning
|
||||
|
||||
The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss.
|
||||
|
||||
!!! Danger "Deprecated functionality"
|
||||
`Edge positioning` (or short Edge) is currently in maintenance mode only (we keep existing functionality alive) and should be considered as deprecated.
|
||||
It will currently not receive new features until either someone stepped forward to take up ownership of that module - or we'll decide to remove edge from freqtrade.
|
||||
|
||||
!!! Warning
|
||||
When using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data.
|
||||
|
||||
!!! Note
|
||||
`Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file.
|
||||
`Edge Positioning` improves the performance of some trading strategies and *decreases* the performance of others.
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose.
|
||||
|
||||
To obtain an edge in the market, a strategy has to make more money than it loses. Making money in trading is not only about *how often* the strategy makes or loses money.
|
||||
|
||||
!!! tip "It doesn't matter how often, but how much!"
|
||||
A bad strategy might make 1 penny in *ten* transactions but lose 1 dollar in *one* transaction. If one only checks the number of winning trades, it would be misleading to think that the strategy is actually making a profit.
|
||||
|
||||
The Edge Positioning module seeks to improve a strategy's winning probability and the money that the strategy will make *on the long run*.
|
||||
|
||||
We raise the following question[^1]:
|
||||
|
||||
!!! Question "Which trade is a better option?"
|
||||
a) A trade with 80% of chance of losing 100\$ and 20% chance of winning 200\$<br/>
|
||||
b) A trade with 100% of chance of losing 30\$
|
||||
|
||||
???+ Info "Answer"
|
||||
The expected value of *a)* is smaller than the expected value of *b)*.<br/>
|
||||
Hence, *b*) represents a smaller loss in the long run.<br/>
|
||||
However, the answer is: *it depends*
|
||||
|
||||
Another way to look at it is to ask a similar question:
|
||||
|
||||
!!! Question "Which trade is a better option?"
|
||||
a) A trade with 80% of chance of winning 100\$ and 20% chance of losing 200\$<br/>
|
||||
b) A trade with 100% of chance of winning 30\$
|
||||
|
||||
Edge positioning tries to answer the hard questions about risk/reward and position size automatically, seeking to minimizes the chances of losing of a given strategy.
|
||||
|
||||
### Trading, winning and losing
|
||||
|
||||
Let's call $o$ the return of a single transaction $o$ where $o \in \mathbb{R}$. The collection $O = \{o_1, o_2, ..., o_N\}$ is the set of all returns of transactions made during a trading session. We say that $N$ is the cardinality of $O$, or, in lay terms, it is the number of transactions made in a trading session.
|
||||
|
||||
!!! Example
|
||||
In a session where a strategy made three transactions we can say that $O = \{3.5, -1, 15\}$. That means that $N = 3$ and $o_1 = 3.5$, $o_2 = -1$, $o_3 = 15$.
|
||||
|
||||
A winning trade is a trade where a strategy *made* money. Making money means that the strategy closed the position in a value that returned a profit, after all deducted fees. Formally, a winning trade will have a return $o_i > 0$. Similarly, a losing trade will have a return $o_j \leq 0$. With that, we can discover the set of all winning trades, $T_{win}$, as follows:
|
||||
|
||||
$$ T_{win} = \{ o \in O | o > 0 \} $$
|
||||
|
||||
Similarly, we can discover the set of losing trades $T_{lose}$ as follows:
|
||||
|
||||
$$ T_{lose} = \{o \in O | o \leq 0\} $$
|
||||
|
||||
!!! Example
|
||||
In a section where a strategy made four transactions $O = \{3.5, -1, 15, 0\}$:<br>
|
||||
$T_{win} = \{3.5, 15\}$<br>
|
||||
$T_{lose} = \{-1, 0\}$<br>
|
||||
|
||||
### Win Rate and Lose Rate
|
||||
|
||||
The win rate $W$ is the proportion of winning trades with respect to all the trades made by a strategy. We use the following function to compute the win rate:
|
||||
|
||||
$$W = \frac{|T_{win}|}{N}$$
|
||||
|
||||
Where $W$ is the win rate, $N$ is the number of trades and, $T_{win}$ is the set of all trades where the strategy made money.
|
||||
|
||||
Similarly, we can compute the rate of losing trades:
|
||||
|
||||
$$
|
||||
L = \frac{|T_{lose}|}{N}
|
||||
$$
|
||||
|
||||
Where $L$ is the lose rate, $N$ is the amount of trades made and, $T_{lose}$ is the set of all trades where the strategy lost money. Note that the above formula is the same as calculating $L = 1 – W$ or $W = 1 – L$
|
||||
|
||||
### Risk Reward Ratio
|
||||
|
||||
Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose. Formally:
|
||||
|
||||
$$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$
|
||||
|
||||
???+ Example "Worked example of $R$ calculation"
|
||||
Let's say that you think that the price of *stonecoin* today is 10.0\$. You believe that, because they will start mining stonecoin, it will go up to 15.0\$ tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to 0\$ tomorrow. You are planning to invest 100\$, which will give you 10 shares (100 / 10).
|
||||
|
||||
Your potential profit is calculated as:
|
||||
|
||||
$\begin{aligned}
|
||||
\text{potential_profit} &= (\text{potential_price} - \text{entry_price}) * \frac{\text{investment}}{\text{entry_price}} \\
|
||||
&= (15 - 10) * (100 / 10) \\
|
||||
&= 50
|
||||
\end{aligned}$
|
||||
|
||||
Since the price might go to 0\$, the 100\$ dollars invested could turn into 0.
|
||||
|
||||
We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$\).
|
||||
|
||||
$\begin{aligned}
|
||||
\text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \frac{\text{investment}}{\text{entry_price}} \\
|
||||
&= (10 - 8.5) * (100 / 10)\\
|
||||
&= 15
|
||||
\end{aligned}$
|
||||
|
||||
We can compute the Risk Reward Ratio as follows:
|
||||
|
||||
$\begin{aligned}
|
||||
R &= \frac{\text{potential_profit}}{\text{potential_loss}}\\
|
||||
&= \frac{50}{15}\\
|
||||
&= 3.33
|
||||
\end{aligned}$<br>
|
||||
What it effectively means is that the strategy have the potential to make 3.33\$ for each 1\$ invested.
|
||||
|
||||
On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows:
|
||||
|
||||
$$ \text{average_profit} = \mu_{win} = \frac{\text{sum_of_profits}}{\text{count_winning_trades}} = \frac{\sum^{o \in T_{win}} o}{|T_{win}|} $$
|
||||
|
||||
Similarly, we can calculate the average loss, $\mu_{lose}$, as follows:
|
||||
|
||||
$$ \text{average_loss} = \mu_{lose} = \frac{\text{sum_of_losses}}{\text{count_losing_trades}} = \frac{\sum^{o \in T_{lose}} o}{|T_{lose}|} $$
|
||||
|
||||
Finally, we can calculate the Risk Reward ratio, $R$, as follows:
|
||||
|
||||
$$ R = \frac{\text{average_profit}}{\text{average_loss}} = \frac{\mu_{win}}{\mu_{lose}}\\ $$
|
||||
|
||||
|
||||
???+ Example "Worked example of $R$ calculation using mean profit/loss"
|
||||
Let's say the strategy that we are using makes an average win $\mu_{win} = 2.06$ and an average loss $\mu_{loss} = 4.11$.<br>
|
||||
We calculate the risk reward ratio as follows:<br>
|
||||
$R = \frac{\mu_{win}}{\mu_{loss}} = \frac{2.06}{4.11} = 0.5012...$
|
||||
|
||||
|
||||
### Expectancy
|
||||
|
||||
By combining the Win Rate $W$ and the Risk Reward ratio $R$ to create an expectancy ratio $E$. A expectance ratio is the expected return of the investment made in a trade. We can compute the value of $E$ as follows:
|
||||
|
||||
$$E = R * W - L$$
|
||||
|
||||
!!! Example "Calculating $E$"
|
||||
Let's say that a strategy has a win rate $W = 0.28$ and a risk reward ratio $R = 5$. What this means is that the strategy is expected to make 5 times the investment around on 28% of the trades it makes. Working out the example:<br>
|
||||
$E = R * W - L = 5 * 0.28 - 0.72 = 0.68$
|
||||
<br>
|
||||
|
||||
The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes 1.68\$ for every 1\$ it loses, on average.
|
||||
|
||||
This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ.
|
||||
|
||||
It is important to remember that any system with an expectancy greater than 0 is profitable using past data. The key is finding one that will be profitable in the future.
|
||||
|
||||
You can also use this value to evaluate the effectiveness of modifications to this system.
|
||||
|
||||
!!! Note
|
||||
It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades.
|
||||
|
||||
## How does it work?
|
||||
|
||||
Edge combines dynamic stoploss, dynamic positions, and whitelist generation into one isolated module which is then applied to the trading strategy. If enabled in config, Edge will go through historical data with a range of stoplosses in order to find buy and sell/stoploss signals. It then calculates win rate and expectancy over *N* trades for each stoploss. Here is an example:
|
||||
|
||||
| Pair | Stoploss | Win Rate | Risk Reward Ratio | Expectancy |
|
||||
|----------|:-------------:|-------------:|------------------:|-----------:|
|
||||
| XZC/ETH | -0.01 | 0.50 |1.176384 | 0.088 |
|
||||
| XZC/ETH | -0.02 | 0.51 |1.115941 | 0.079 |
|
||||
| XZC/ETH | -0.03 | 0.52 |1.359670 | 0.228 |
|
||||
| XZC/ETH | -0.04 | 0.51 |1.234539 | 0.117 |
|
||||
|
||||
The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at $3%$ leads to the maximum expectancy according to historical data.
|
||||
|
||||
Edge module then forces stoploss value it evaluated to your strategy dynamically.
|
||||
|
||||
### Position size
|
||||
|
||||
Edge dictates the amount at stake for each trade to the bot according to the following factors:
|
||||
|
||||
- Allowed capital at risk
|
||||
- Stoploss
|
||||
|
||||
Allowed capital at risk is calculated as follows:
|
||||
|
||||
```
|
||||
Allowed capital at risk = (Capital available_percentage) X (Allowed risk per trade)
|
||||
```
|
||||
|
||||
Stoploss is calculated as described above with respect to historical data.
|
||||
|
||||
The position size is calculated as follows:
|
||||
|
||||
```
|
||||
Position size = (Allowed capital at risk) / Stoploss
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet. The capital available percentage is $50%$ and the allowed risk per trade is $1\%$. Thus, the available capital for trading is $10 * 0.5 = 5$ **ETH** and the allowed capital at risk would be $5 * 0.01 = 0.05$ **ETH**.
|
||||
|
||||
- **Trade 1:** The strategy detects a new buy signal in the **XLM/ETH** market. `Edge Positioning` calculates a stoploss of $2\%$ and a position of $0.05 / 0.02 = 2.5$ **ETH**. The bot takes a position of $2.5$ **ETH** in the **XLM/ETH** market.
|
||||
|
||||
- **Trade 2:** The strategy detects a buy signal on the **BTC/ETH** market while **Trade 1** is still open. `Edge Positioning` calculates the stoploss of $4\%$ on this market. Thus, **Trade 2** position size is $0.05 / 0.04 = 1.25$ **ETH**.
|
||||
|
||||
!!! Tip "Available Capital $\neq$ Available in wallet"
|
||||
The available capital for trading didn't change in **Trade 2** even with **Trade 1** still open. The available capital **is not** the free amount in the wallet.
|
||||
|
||||
- **Trade 3:** The strategy detects a buy signal in the **ADA/ETH** market. `Edge Positioning` calculates a stoploss of $1\%$ and a position of $0.05 / 0.01 = 5$ **ETH**. Since **Trade 1** has $2.5$ **ETH** blocked and **Trade 2** has $1.25$ **ETH** blocked, there is only $5 - 1.25 - 2.5 = 1.25$ **ETH** available. Hence, the position size of **Trade 3** is $1.25$ **ETH**.
|
||||
|
||||
!!! Tip "Available Capital Updates"
|
||||
The available capital does not change before a position is sold. After a trade is closed the Available Capital goes up if the trade was profitable or goes down if the trade was a loss.
|
||||
|
||||
- The strategy detects a sell signal in the **XLM/ETH** market. The bot exits **Trade 1** for a profit of $1$ **ETH**. The total capital in the wallet becomes $11$ **ETH** and the available capital for trading becomes $5.5$ **ETH**.
|
||||
|
||||
- **Trade 4** The strategy detects a new buy signal int the **XLM/ETH** market. `Edge Positioning` calculates the stoploss of $2\%$, and the position size of $0.055 / 0.02 = 2.75$ **ETH**.
|
||||
|
||||
## Edge command reference
|
||||
|
||||
--8<-- "commands/edge.md"
|
||||
|
||||
## Configurations
|
||||
|
||||
Edge module has following configuration options:
|
||||
|
||||
| Parameter | Description |
|
||||
|------------|-------------|
|
||||
| `enabled` | If true, then Edge will run periodically. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `process_throttle_secs` | How often should Edge run in seconds. <br>*Defaults to `3600` (once per hour).* <br> **Datatype:** Integer
|
||||
| `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy. <br> **Note** that it downloads historical data so increasing this number would lead to slowing down the bot. <br>*Defaults to `7`.* <br> **Datatype:** Integer
|
||||
| `allowed_risk` | Ratio of allowed risk per trade. <br>*Defaults to `0.01` (1%)).* <br> **Datatype:** Float
|
||||
| `stoploss_range_min` | Minimum stoploss. <br>*Defaults to `-0.01`.* <br> **Datatype:** Float
|
||||
| `stoploss_range_max` | Maximum stoploss. <br>*Defaults to `-0.10`.* <br> **Datatype:** Float
|
||||
| `stoploss_range_step` | As an example if this is set to -0.01 then Edge will test the strategy for `[-0.01, -0,02, -0,03 ..., -0.09, -0.10]` ranges. <br> **Note** than having a smaller step means having a bigger range which could lead to slow calculation. <br> If you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10. <br>*Defaults to `-0.001`.* <br> **Datatype:** Float
|
||||
| `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate. <br>This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio. <br>*Defaults to `0.60`.* <br> **Datatype:** Float
|
||||
| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number. <br>Having an expectancy of 0.20 means if you put 10\$ on a trade you expect a 12\$ return. <br>*Defaults to `0.20`.* <br> **Datatype:** Float
|
||||
| `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. <br>Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. <br>*Defaults to `10` (it is highly recommended not to decrease this number).* <br> **Datatype:** Integer
|
||||
| `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.<br>**NOTICE:** While configuring this value, you should take into consideration your timeframe. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).<br>*Defaults to `1440` (one day).* <br> **Datatype:** Integer
|
||||
| `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.<br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
|
||||
## Running Edge independently
|
||||
|
||||
You can run Edge independently in order to see in details the result. Here is an example:
|
||||
|
||||
``` bash
|
||||
freqtrade edge
|
||||
```
|
||||
|
||||
An example of its output:
|
||||
|
||||
| **pair** | **stoploss** | **win rate** | **risk reward ratio** | **required risk reward** | **expectancy** | **total number of trades** | **average duration (min)** |
|
||||
|:----------|-----------:|-----------:|--------------------:|-----------------------:|-------------:|-----------------:|---------------:|
|
||||
| **AGI/BTC** | -0.02 | 0.64 | 5.86 | 0.56 | 3.41 | 14 | 54 |
|
||||
| **NXS/BTC** | -0.03 | 0.64 | 2.99 | 0.57 | 1.54 | 11 | 26 |
|
||||
| **LEND/BTC** | -0.02 | 0.82 | 2.05 | 0.22 | 1.50 | 11 | 36 |
|
||||
| **VIA/BTC** | -0.01 | 0.55 | 3.01 | 0.83 | 1.19 | 11 | 48 |
|
||||
| **MTH/BTC** | -0.09 | 0.56 | 2.82 | 0.80 | 1.12 | 18 | 52 |
|
||||
| **ARDR/BTC** | -0.04 | 0.42 | 3.14 | 1.40 | 0.73 | 12 | 42 |
|
||||
| **BCPT/BTC** | -0.01 | 0.71 | 1.34 | 0.40 | 0.67 | 14 | 30 |
|
||||
| **WINGS/BTC** | -0.02 | 0.56 | 1.97 | 0.80 | 0.65 | 27 | 42 |
|
||||
| **VIBE/BTC** | -0.02 | 0.83 | 0.91 | 0.20 | 0.59 | 12 | 35 |
|
||||
| **MCO/BTC** | -0.02 | 0.79 | 0.97 | 0.27 | 0.55 | 14 | 31 |
|
||||
| **GNT/BTC** | -0.02 | 0.50 | 2.06 | 1.00 | 0.53 | 18 | 24 |
|
||||
| **HOT/BTC** | -0.01 | 0.17 | 7.72 | 4.81 | 0.50 | 209 | 7 |
|
||||
| **SNM/BTC** | -0.03 | 0.71 | 1.06 | 0.42 | 0.45 | 17 | 38 |
|
||||
| **APPC/BTC** | -0.02 | 0.44 | 2.28 | 1.27 | 0.44 | 25 | 43 |
|
||||
| **NEBL/BTC** | -0.03 | 0.63 | 1.29 | 0.58 | 0.44 | 19 | 59 |
|
||||
|
||||
Edge produced the above table by comparing `calculate_since_number_of_days` to `minimum_expectancy` to find `min_trade_number` historical information based on the config file. The timerange Edge uses for its comparisons can be further limited by using the `--timerange` switch.
|
||||
|
||||
In live and dry-run modes, after the `process_throttle_secs` has passed, Edge will again process `calculate_since_number_of_days` against `minimum_expectancy` to find `min_trade_number`. If no `min_trade_number` is found, the bot will return "whitelist empty". Depending on the trade strategy being deployed, "whitelist empty" may be return much of the time - or *all* of the time. The use of Edge may also cause trading to occur in bursts, though this is rare.
|
||||
|
||||
If you encounter "whitelist empty" a lot, condsider tuning `calculate_since_number_of_days`, `minimum_expectancy` and `min_trade_number` to align to the trading frequency of your strategy.
|
||||
|
||||
### Update cached pairs with the latest data
|
||||
|
||||
Edge requires historic data the same way as backtesting does.
|
||||
Please refer to the [Data Downloading](data-download.md) section of the documentation for details.
|
||||
|
||||
### Precising stoploss range
|
||||
|
||||
```bash
|
||||
freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step
|
||||
```
|
||||
|
||||
### Advanced use of timerange
|
||||
|
||||
```bash
|
||||
freqtrade edge --timerange=20181110-20181113
|
||||
```
|
||||
|
||||
Doing `--timerange=-20190901` will get all available data until September 1st (excluding September 1st 2019).
|
||||
|
||||
The full timerange specification:
|
||||
|
||||
* Use tickframes till 2018/01/31: `--timerange=-20180131`
|
||||
* Use tickframes since 2018/01/31: `--timerange=20180131-`
|
||||
* Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
|
||||
* Use tickframes between POSIX timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600`
|
||||
|
||||
|
||||
[^1]: Question extracted from MIT Opencourseware S096 - Mathematics with applications in Finance: https://ocw.mit.edu/courses/mathematics/18-s096-topics-in-mathematics-with-applications-in-finance-fall-2013/
|
||||
+23
-2
@@ -339,13 +339,13 @@ This needs to be configured like this:
|
||||
```json
|
||||
"exchange": {
|
||||
"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",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
* 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.
|
||||
|
||||
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 +363,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.
|
||||
* 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
|
||||
|
||||
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.
|
||||
|
||||
+8
-14
@@ -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.
|
||||
* 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
|
||||
|
||||
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).
|
||||
@@ -276,20 +284,6 @@ Example: 4% profit 650 times vs 0,3% profit a trade 10000 times in a year. If we
|
||||
Example:
|
||||
`freqtrade --config config.json --strategy SampleStrategy --hyperopt SampleHyperopt -e 1000 --timerange 20190601-20200601`
|
||||
|
||||
## Edge module
|
||||
|
||||
### Edge implements interesting approach for controlling position size, is there any theory behind it?
|
||||
|
||||
The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members.
|
||||
|
||||
You can find further info on expectancy, win rate, risk management and position size in the following sources:
|
||||
|
||||
- https://www.tradeciety.com/ultimate-math-guide-for-traders/
|
||||
- https://samuraitradingacademy.com/trading-expectancy/
|
||||
- https://www.learningmarkets.com/determining-expectancy-in-your-trading/
|
||||
- https://www.lonestocktrader.com/make-money-trading-positive-expectancy/
|
||||
- https://www.babypips.com/trading/trade-expectancy-matter
|
||||
|
||||
## Official channels
|
||||
|
||||
Freqtrade is using exclusively the following official channels:
|
||||
|
||||
+5
-6
@@ -5,10 +5,10 @@
|
||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||
|
||||
<!-- Place this tag where you want the button to render. -->
|
||||
<a class="github-button" href="https://github.com/freqtrade/freqtrade" data-icon="octicon-star" data-size="large" aria-label="Star freqtrade/freqtrade on GitHub">Star</a>
|
||||
<a class="github-button" href="https://github.com/freqtrade/freqtrade/fork" data-icon="octicon-repo-forked" data-size="large" aria-label="Fork freqtrade/freqtrade on GitHub">Fork</a>
|
||||
<a class="github-button" href="https://github.com/freqtrade/freqtrade/archive/stable.zip" data-icon="octicon-cloud-download" data-size="large" aria-label="Download freqtrade/freqtrade on GitHub">Download</a>
|
||||
<!-- GitHub action buttons -->
|
||||
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }
|
||||
[:octicons-repo-forked-16: Fork](https://github.com/freqtrade/freqtrade/fork){ .md-button .md-button--sm }
|
||||
[:octicons-download-16: Download](https://github.com/freqtrade/freqtrade/archive/stable.zip){ .md-button .md-button--sm }
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -31,7 +31,6 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
||||
- Optimize: Find the best parameters for your strategy using hyperoptimization which employs machine learning methods. You can optimize buy, sell, take profit (ROI), stop-loss and trailing stop-loss parameters for your strategy.
|
||||
- Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade.
|
||||
- Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode).
|
||||
- Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital.
|
||||
- Control/Monitor: Use Telegram or a WebUI (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.).
|
||||
- Analyze: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md).
|
||||
|
||||
@@ -88,7 +87,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
|
||||
|
||||
Alternatively
|
||||
|
||||
- Python 3.10+
|
||||
- Python 3.11+
|
||||
- pip (pip3)
|
||||
- git
|
||||
- TA-Lib
|
||||
|
||||
@@ -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).
|
||||
|
||||
!!! 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.
|
||||
|
||||
!!! Warning "Up-to-date clock"
|
||||
@@ -42,7 +42,7 @@ These requirements apply to both [Script Installation](#script-installation) and
|
||||
|
||||
### 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/)
|
||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
||||
@@ -54,7 +54,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.
|
||||
|
||||
!!! 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"
|
||||
#### Install necessary dependencies
|
||||
@@ -179,7 +179,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr
|
||||
** --install **
|
||||
|
||||
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`
|
||||
* Setup your virtualenv under `.venv/`
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
{{ super() }}
|
||||
|
||||
<!-- Place this tag in your head or just before your close body tag. -->
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
|
||||
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
markdown==3.8
|
||||
markdown==3.8.2
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.6.14
|
||||
mkdocs-material==9.6.16
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.15
|
||||
pymdown-extensions==10.16
|
||||
jinja2==3.1.6
|
||||
mike==2.1.3
|
||||
|
||||
@@ -190,9 +190,6 @@ delete_trade
|
||||
|
||||
:param trade_id: Deletes the trade with this ID from the database.
|
||||
|
||||
edge
|
||||
Return information about edge.
|
||||
|
||||
forcebuy
|
||||
Buy an asset.
|
||||
|
||||
@@ -368,7 +365,6 @@ All endpoints in the below table need to be prefixed with the base URL of the AP
|
||||
| `/blacklist` | GET | Show the current blacklist.
|
||||
| `/blacklist` | POST | Adds the specified pair to the blacklist.<br/>*Params:*<br/>- `pair` (`str`)
|
||||
| `/blacklist` | DELETE | Deletes the specified list of pairs from the blacklist.<br/>*Params:*<br/>- `[pair,pair]` (`list[str]`)
|
||||
| `/edge` | GET | Show validated pairs by Edge if it is enabled.
|
||||
| `/pair_candles` | GET | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha**
|
||||
| `/pair_candles` | POST | Returns dataframe for a pair / timeframe combination while the bot is running, filtered by a provided list of columns to return. **Alpha**<br/>*Params:*<br/>- `<column_list>` (`list[str]`)
|
||||
| `/pair_history` | GET | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha**
|
||||
|
||||
+1
-1
@@ -256,4 +256,4 @@ The new stoploss value will be applied to open trades (and corresponding log-mes
|
||||
|
||||
### Limitations
|
||||
|
||||
Stoploss values cannot be changed if `trailing_stop` is enabled and the stoploss has already been adjusted, or if [Edge](edge.md) is enabled (since Edge would recalculate stoploss based on the current market situation).
|
||||
Stoploss values cannot be changed if `trailing_stop` is enabled and the stoploss has already been adjusted.
|
||||
|
||||
+37
-14
@@ -174,17 +174,27 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
## Enter Tag
|
||||
|
||||
When your strategy has multiple buy signals, you can name the signal that triggered.
|
||||
Then you can access your buy signal on `custom_exit`
|
||||
When your strategy has multiple entry signals, you can name the signal that triggered.
|
||||
Then you can access your entry signal on `custom_exit`
|
||||
|
||||
```python
|
||||
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['rsi'] < 35) &
|
||||
(dataframe['volume'] > 0)
|
||||
),
|
||||
['enter_long', 'enter_tag']] = (1, 'buy_signal_rsi')
|
||||
signal_rsi
|
||||
| signal_bblower
|
||||
# ... additional signals to enter a long position
|
||||
)
|
||||
& (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
|
||||
|
||||
@@ -192,14 +202,17 @@ def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_r
|
||||
current_profit: float, **kwargs):
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
if trade.enter_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80:
|
||||
return 'sell_signal_rsi'
|
||||
if "long_signal_rsi" in trade.enter_tag and last_candle["rsi"] > 80:
|
||||
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
|
||||
|
||||
```
|
||||
|
||||
!!! 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
|
||||
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
|
||||
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['rsi'] > 70) &
|
||||
(dataframe['volume'] > 0)
|
||||
),
|
||||
['exit_long', 'exit_tag']] = (1, 'exit_rsi')
|
||||
rsi_exit_signal
|
||||
| ema_exit_signal
|
||||
# ... additional signals to exit a long position
|
||||
) &
|
||||
(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
|
||||
```
|
||||
|
||||
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
|
||||
`exit_reason` is limited to 100 characters, remaining data will be truncated.
|
||||
|
||||
@@ -1068,7 +1068,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
||||
``` python
|
||||
from freqtrade.persistence import Trade
|
||||
from datetime import timedelta, datetime, timezone
|
||||
# Put the above lines a the top of the strategy file, next to all the other imports
|
||||
# Put the above lines at the top of the strategy file, next to all the other imports
|
||||
# --------
|
||||
|
||||
# Within populate indicators (or populate_entry_trend):
|
||||
|
||||
@@ -19,3 +19,31 @@
|
||||
#available-endpoints ~ .md-typeset__scrollwrap .md-typeset__table th:first-of-type {
|
||||
width: 35% !important;
|
||||
}
|
||||
|
||||
|
||||
.md-typeset .md-button--sm {
|
||||
padding: 0.2em 1em;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background-color: #f6f8fa;
|
||||
color: #24292f;
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 0.25em;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.md-typeset .md-button--sm:hover {
|
||||
background-color: #e5eaee;
|
||||
border-color: #d1d9e0;
|
||||
text-decoration: none;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
.md-typeset .md-button--sm:active {
|
||||
background-color: #ebecf0;
|
||||
border-color: #afb8c1;
|
||||
box-shadow: inset 0 1px 0 rgba(175, 184, 193, 0.2);
|
||||
}
|
||||
|
||||
+4
-17
@@ -188,7 +188,7 @@ You can create your own keyboard in `config.json`:
|
||||
!!! Note "Supported Commands"
|
||||
Only the following commands are allowed. Command arguments are not supported!
|
||||
|
||||
`/start`, `/pause`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir`
|
||||
`/start`, `/pause`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/help`, `/version`, `/marketdir`
|
||||
|
||||
## Telegram commands
|
||||
|
||||
@@ -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.
|
||||
| **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_[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
|
||||
| `/balance` | Show bot managed balance per currency
|
||||
| `/balance full` | Show account balance per currency
|
||||
@@ -240,7 +241,6 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
| `/entries` | Shows Wins / losses by Exit reason as well as Avg. holding durations for buys and sells
|
||||
| `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing.
|
||||
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||
| `/edge` | Show validated pairs by Edge if it is enabled.
|
||||
|
||||
## Telegram commands in action
|
||||
|
||||
@@ -310,6 +310,8 @@ current max
|
||||
|
||||
### /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.
|
||||
|
||||
> **ROI:** Close trades
|
||||
@@ -451,21 +453,6 @@ Use `/reload_config` to reset the blacklist.
|
||||
> Using blacklist `StaticPairList` with 2 pairs
|
||||
>`DODGE/BTC`, `HOT/BTC`.
|
||||
|
||||
### /edge
|
||||
|
||||
Shows pairs validated by Edge along with their corresponding win-rate, expectancy and stoploss values.
|
||||
|
||||
> **Edge only validated following pairs:**
|
||||
```
|
||||
Pair Winrate Expectancy Stoploss
|
||||
-------- --------- ------------ ----------
|
||||
DOCK/ETH 0.522727 0.881821 -0.03
|
||||
PHX/ETH 0.677419 0.560488 -0.03
|
||||
HOT/ETH 0.733333 0.490492 -0.03
|
||||
HC/ETH 0.588235 0.280988 -0.02
|
||||
ARDR/ETH 0.366667 0.143059 -0.01
|
||||
```
|
||||
|
||||
### /version
|
||||
|
||||
> **Version:** `0.14.3`
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Utility Subcommands
|
||||
|
||||
Besides the Live-Trade and Dry-Run run modes, the `backtesting`, `edge` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section.
|
||||
Besides the Live-Trade and Dry-Run run modes, the `backtesting` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section.
|
||||
|
||||
## Create userdir
|
||||
|
||||
|
||||
+9
-52
@@ -117,9 +117,9 @@ Different payloads can be configured for different events. Not all fields are ne
|
||||
|
||||
## 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:
|
||||
|
||||
* `trade_id`
|
||||
@@ -162,31 +162,9 @@ Possible parameters are:
|
||||
* `current_rate`
|
||||
* `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.
|
||||
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.
|
||||
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`
|
||||
@@ -195,34 +173,9 @@ Possible parameters are:
|
||||
* `direction`
|
||||
* `leverage`
|
||||
* `gain`
|
||||
* `limit`
|
||||
* `amount`
|
||||
* `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`
|
||||
* `amount`
|
||||
* `open_rate`
|
||||
* `current_rate`
|
||||
* `profit_amount`
|
||||
* `profit_ratio`
|
||||
@@ -230,10 +183,14 @@ Possible parameters are:
|
||||
* `base_currency`
|
||||
* `quote_currency`
|
||||
* `fiat_currency`
|
||||
* `enter_tag`
|
||||
* `exit_reason`
|
||||
* `order_type`
|
||||
* `open_date`
|
||||
* `close_date`
|
||||
* `sub_trade`
|
||||
* `is_final_exit`
|
||||
|
||||
|
||||
### Exit cancel
|
||||
|
||||
@@ -246,7 +203,7 @@ Possible parameters are:
|
||||
* `direction`
|
||||
* `leverage`
|
||||
* `gain`
|
||||
* `limit`
|
||||
* `order_rate`
|
||||
* `amount`
|
||||
* `open_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.
|
||||
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
|
||||
|
||||
@@ -42,7 +42,7 @@ cd freqtrade
|
||||
|
||||
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 and 3.12) and for 64bit 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.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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2025.5"
|
||||
__version__ = "2025.7"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
__main__.py for Freqtrade
|
||||
To launch Freqtrade as a module
|
||||
|
||||
> python -m freqtrade (with Python >= 3.10)
|
||||
> python -m freqtrade (with Python >= 3.11)
|
||||
"""
|
||||
|
||||
from freqtrade import main
|
||||
|
||||
@@ -57,6 +57,7 @@ ARGS_BACKTEST = [
|
||||
"backtest_breakdown",
|
||||
"backtest_cache",
|
||||
"freqai_backtest_live_models",
|
||||
"backtest_notes",
|
||||
]
|
||||
|
||||
ARGS_HYPEROPT = [
|
||||
@@ -81,7 +82,7 @@ ARGS_HYPEROPT = [
|
||||
"early_stop",
|
||||
]
|
||||
|
||||
ARGS_EDGE = [*ARGS_COMMON_OPTIMIZE, "stoploss_range"]
|
||||
ARGS_EDGE = [*ARGS_COMMON_OPTIMIZE]
|
||||
|
||||
ARGS_LIST_STRATEGIES = [
|
||||
"strategy_path",
|
||||
@@ -250,31 +251,33 @@ ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_s
|
||||
ARGS_LOOKAHEAD_ANALYSIS = [
|
||||
a
|
||||
for a in ARGS_BACKTEST
|
||||
if a not in ("position_stacking", "backtest_cache", "backtest_breakdown")
|
||||
if a not in ("position_stacking", "backtest_cache", "backtest_breakdown", "backtest_notes")
|
||||
] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"]
|
||||
|
||||
ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"]
|
||||
|
||||
# Command level configs - keep at the bottom of the above definitions
|
||||
NO_CONF_REQURIED = [
|
||||
"backtest-filter",
|
||||
"backtesting-show",
|
||||
"convert-data",
|
||||
"convert-trade-data",
|
||||
"download-data",
|
||||
"list-timeframes",
|
||||
"hyperopt-list",
|
||||
"hyperopt-show",
|
||||
"list-data",
|
||||
"list-freqaimodels",
|
||||
"list-hyperoptloss",
|
||||
"list-markets",
|
||||
"list-pairs",
|
||||
"list-strategies",
|
||||
"list-freqaimodels",
|
||||
"list-hyperoptloss",
|
||||
"list-data",
|
||||
"hyperopt-list",
|
||||
"hyperopt-show",
|
||||
"backtest-filter",
|
||||
"list-timeframes",
|
||||
"plot-dataframe",
|
||||
"plot-profit",
|
||||
"show-trades",
|
||||
"trades-to-ohlcv",
|
||||
"install-ui",
|
||||
"strategy-updater",
|
||||
"trades-to-ohlcv",
|
||||
]
|
||||
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||
@@ -310,8 +313,6 @@ class Arguments:
|
||||
# (see https://bugs.python.org/issue16399)
|
||||
# Allow no-config for certain commands (like downloading / plotting)
|
||||
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:
|
||||
user_dir = parsed_arg.user_data_dir
|
||||
else:
|
||||
@@ -324,7 +325,9 @@ class Arguments:
|
||||
else:
|
||||
# Else use "config.json".
|
||||
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]
|
||||
|
||||
return parsed_arg
|
||||
@@ -505,7 +508,9 @@ class Arguments:
|
||||
|
||||
# Add edge subcommand
|
||||
edge_cmd = subparsers.add_parser(
|
||||
"edge", help="Edge module.", parents=[_common_parser, _strategy_parser]
|
||||
"edge",
|
||||
help="Edge module. No longer part of Freqtrade",
|
||||
parents=[_common_parser, _strategy_parser],
|
||||
)
|
||||
edge_cmd.set_defaults(func=start_edge)
|
||||
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
|
||||
|
||||
@@ -204,6 +204,11 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help="Export backtest results (default: trades).",
|
||||
choices=constants.EXPORT_OPTIONS,
|
||||
),
|
||||
"backtest_notes": Arg(
|
||||
"--notes",
|
||||
help="Add notes to the backtest results.",
|
||||
metavar="TEXT",
|
||||
),
|
||||
"exportfilename": Arg(
|
||||
"--export-filename",
|
||||
"--backtest-filename",
|
||||
@@ -235,13 +240,6 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
default=constants.BACKTEST_CACHE_DEFAULT,
|
||||
choices=constants.BACKTEST_CACHE_AGE,
|
||||
),
|
||||
# Edge
|
||||
"stoploss_range": Arg(
|
||||
"--stoplosses",
|
||||
help="Defines a range of stoploss values against which edge will assess the strategy. "
|
||||
'The format is "min,max,step" (without any space). '
|
||||
"Example: `--stoplosses=-0.01,-0.1,-0.001`",
|
||||
),
|
||||
# Hyperopt
|
||||
"hyperopt": Arg(
|
||||
"--hyperopt",
|
||||
|
||||
@@ -129,15 +129,10 @@ def start_edge(args: dict[str, Any]) -> None:
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.optimize.edge_cli import EdgeCli
|
||||
|
||||
# Initialize configuration
|
||||
config = setup_optimize_configuration(args, RunMode.EDGE)
|
||||
logger.info("Starting freqtrade in Edge mode")
|
||||
|
||||
# Initialize Edge object
|
||||
edge_cli = EdgeCli(config)
|
||||
edge_cli.start()
|
||||
raise ConfigurationError(
|
||||
"The Edge module has been deprecated in 2023.9 and removed in 2025.6. "
|
||||
"All functionalities of edge have been removed."
|
||||
)
|
||||
|
||||
|
||||
def start_lookahead_analysis(args: dict[str, Any]) -> None:
|
||||
|
||||
@@ -423,10 +423,6 @@ CONF_SCHEMA = {
|
||||
"description": "Exchange configuration.",
|
||||
"$ref": "#/definitions/exchange",
|
||||
},
|
||||
"edge": {
|
||||
"description": "Edge configuration.",
|
||||
"$ref": "#/definitions/edge",
|
||||
},
|
||||
"log_config": {
|
||||
"description": "Logging configuration.",
|
||||
"$ref": "#/definitions/logging",
|
||||
@@ -913,30 +909,22 @@ CONF_SCHEMA = {
|
||||
},
|
||||
"ccxt_config": {"description": "CCXT configuration settings.", "type": "object"},
|
||||
"ccxt_async_config": {
|
||||
"description": "CCXT asynchronous configuration settings.",
|
||||
"description": (
|
||||
"CCXT asynchronous configuration settings."
|
||||
"Usually ccxt_config should be used instead."
|
||||
),
|
||||
"type": "object",
|
||||
},
|
||||
"ccxt_sync_config": {
|
||||
"description": (
|
||||
"CCXT synchronous configuration settings. "
|
||||
"Usually ccxt_config should be used instead."
|
||||
),
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"edge": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"process_throttle_secs": {"type": "integer", "minimum": 600},
|
||||
"calculate_since_number_of_days": {"type": "integer"},
|
||||
"allowed_risk": {"type": "number"},
|
||||
"stoploss_range_min": {"type": "number"},
|
||||
"stoploss_range_max": {"type": "number"},
|
||||
"stoploss_range_step": {"type": "number"},
|
||||
"minimum_winrate": {"type": "number"},
|
||||
"minimum_expectancy": {"type": "number"},
|
||||
"min_trade_number": {"type": "number"},
|
||||
"max_trade_duration_minute": {"type": "integer"},
|
||||
"remove_pumps": {"type": "boolean"},
|
||||
},
|
||||
"required": ["process_throttle_secs", "allowed_risk"],
|
||||
},
|
||||
"logging": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# flake8: noqa: F401
|
||||
|
||||
from freqtrade.configuration.config_secrets import sanitize_config
|
||||
from freqtrade.configuration.config_secrets import remove_exchange_credentials, sanitize_config
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||
from freqtrade.configuration.configuration import Configuration
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.constants import Config, ExchangeConfig
|
||||
|
||||
|
||||
_SENSITIVE_KEYS = [
|
||||
"exchange.key",
|
||||
"exchange.api_key",
|
||||
"exchange.apiKey",
|
||||
"exchange.secret",
|
||||
"exchange.password",
|
||||
"exchange.uid",
|
||||
"exchange.account_id",
|
||||
"exchange.accountId",
|
||||
"exchange.wallet_address",
|
||||
"exchange.walletAddress",
|
||||
"exchange.private_key",
|
||||
"exchange.privateKey",
|
||||
"telegram.token",
|
||||
"telegram.chat_id",
|
||||
"discord.webhook_url",
|
||||
"api_server.password",
|
||||
"webhook.url",
|
||||
]
|
||||
|
||||
|
||||
def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
|
||||
@@ -12,27 +33,8 @@ def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
|
||||
"""
|
||||
if show_sensitive:
|
||||
return config
|
||||
keys_to_remove = [
|
||||
"exchange.key",
|
||||
"exchange.api_key",
|
||||
"exchange.apiKey",
|
||||
"exchange.secret",
|
||||
"exchange.password",
|
||||
"exchange.uid",
|
||||
"exchange.account_id",
|
||||
"exchange.accountId",
|
||||
"exchange.wallet_address",
|
||||
"exchange.walletAddress",
|
||||
"exchange.private_key",
|
||||
"exchange.privateKey",
|
||||
"telegram.token",
|
||||
"telegram.chat_id",
|
||||
"discord.webhook_url",
|
||||
"api_server.password",
|
||||
"webhook.url",
|
||||
]
|
||||
config = deepcopy(config)
|
||||
for key in keys_to_remove:
|
||||
for key in _SENSITIVE_KEYS:
|
||||
if "." in key:
|
||||
nested_keys = key.split(".")
|
||||
nested_config = config
|
||||
@@ -45,3 +47,21 @@ def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
|
||||
config[key] = "REDACTED"
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def remove_exchange_credentials(exchange_config: ExchangeConfig, dry_run: bool) -> None:
|
||||
"""
|
||||
Removes exchange keys from the configuration and specifies dry-run
|
||||
Used for backtesting / hyperopt and utils.
|
||||
Modifies the input dict!
|
||||
:param exchange_config: Exchange configuration
|
||||
:param dry_run: If True, remove sensitive keys from the exchange configuration
|
||||
"""
|
||||
if not dry_run:
|
||||
return
|
||||
|
||||
for key in [k for k in _SENSITIVE_KEYS if k.startswith("exchange.")]:
|
||||
if "." in key:
|
||||
key1 = key.removeprefix("exchange.")
|
||||
if key1 in exchange_config:
|
||||
exchange_config[key1] = ""
|
||||
|
||||
@@ -99,14 +99,12 @@ def validate_config_consistency(conf: dict[str, Any], *, preliminary: bool = Fal
|
||||
|
||||
def _validate_unlimited_amount(conf: dict[str, Any]) -> None:
|
||||
"""
|
||||
If edge is disabled, either max_open_trades or stake_amount need to be set.
|
||||
Either max_open_trades or stake_amount need to be set.
|
||||
:raise: ConfigurationError if config validation failed
|
||||
"""
|
||||
if (
|
||||
not conf.get("edge", {}).get("enabled")
|
||||
and (conf.get("max_open_trades") == float("inf") or conf.get("max_open_trades") == -1)
|
||||
and conf.get("stake_amount") == UNLIMITED_STAKE_AMOUNT
|
||||
):
|
||||
conf.get("max_open_trades") == float("inf") or conf.get("max_open_trades") == -1
|
||||
) and conf.get("stake_amount") == UNLIMITED_STAKE_AMOUNT:
|
||||
raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.")
|
||||
|
||||
|
||||
@@ -164,12 +162,9 @@ def _validate_edge(conf: dict[str, Any]) -> None:
|
||||
Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists.
|
||||
"""
|
||||
|
||||
if not conf.get("edge", {}).get("enabled"):
|
||||
return
|
||||
|
||||
if not conf.get("use_exit_signal", True):
|
||||
if conf.get("edge", {}).get("enabled"):
|
||||
raise ConfigurationError(
|
||||
"Edge requires `use_exit_signal` to be True, otherwise no sells will happen."
|
||||
"Edge is no longer supported and has been removed from Freqtrade with 2025.6."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
This module contains the configuration class
|
||||
"""
|
||||
|
||||
import ast
|
||||
import logging
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
@@ -19,10 +18,7 @@ from freqtrade.constants import Config
|
||||
from freqtrade.enums import (
|
||||
NON_UTIL_MODES,
|
||||
TRADE_MODES,
|
||||
CandleType,
|
||||
MarginMode,
|
||||
RunMode,
|
||||
TradingMode,
|
||||
)
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.loggers import setup_logging
|
||||
@@ -310,17 +306,10 @@ class Configuration:
|
||||
("backtest_cache", "Parameter --cache={} detected ..."),
|
||||
("disableparamexport", "Parameter --disableparamexport detected: {} ..."),
|
||||
("freqai_backtest_live_models", "Parameter --freqai-backtest-live-models detected ..."),
|
||||
("backtest_notes", "Parameter --notes detected: {} ..."),
|
||||
]
|
||||
self._args_to_config_loop(config, configurations)
|
||||
|
||||
# Edge section:
|
||||
if self.args.get("stoploss_range"):
|
||||
txt_range = ast.literal_eval(self.args["stoploss_range"])
|
||||
config["edge"].update({"stoploss_range_min": txt_range[0]})
|
||||
config["edge"].update({"stoploss_range_max": txt_range[1]})
|
||||
config["edge"].update({"stoploss_range_step": txt_range[2]})
|
||||
logger.info("Parameter --stoplosses detected: %s ...", self.args["stoploss_range"])
|
||||
|
||||
# Hyperopt section
|
||||
|
||||
configurations = [
|
||||
@@ -405,11 +394,6 @@ class Configuration:
|
||||
self._args_to_config(
|
||||
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(
|
||||
config, argname="candle_types", logstring="Detected --candle-types: {}"
|
||||
)
|
||||
|
||||
@@ -159,16 +159,6 @@ def process_temporary_deprecated_settings(config: Config) -> None:
|
||||
process_removed_setting(
|
||||
config, "ask_strategy", "ignore_roi_if_buy_signal", None, "ignore_roi_if_entry_signal"
|
||||
)
|
||||
if config.get("edge", {}).get(
|
||||
"enabled", False
|
||||
) and "capital_available_percentage" in config.get("edge", {}):
|
||||
raise ConfigurationError(
|
||||
"DEPRECATED: "
|
||||
"Using 'edge.capital_available_percentage' has been deprecated in favor of "
|
||||
"'tradable_balance_ratio'. Please migrate your configuration to "
|
||||
"'tradable_balance_ratio' and remove 'capital_available_percentage' "
|
||||
"from the edge configuration."
|
||||
)
|
||||
if "ticker_interval" in config:
|
||||
raise ConfigurationError(
|
||||
"DEPRECATED: 'ticker_interval' detected. "
|
||||
|
||||
@@ -43,15 +43,27 @@ def _flat_vars_to_nested_dict(env_dict: dict[str, Any], prefix: str) -> dict[str
|
||||
:return: Nested dict based on available and relevant variables.
|
||||
"""
|
||||
no_convert = ["CHAT_ID", "PASSWORD"]
|
||||
ccxt_config_keys = ["ccxt_config", "ccxt_sync_config", "ccxt_async_config"]
|
||||
relevant_vars: dict[str, Any] = {}
|
||||
|
||||
for env_var, val in sorted(env_dict.items()):
|
||||
if env_var.startswith(prefix):
|
||||
logger.info(f"Loading variable '{env_var}'")
|
||||
key = env_var.replace(prefix, "")
|
||||
for k in reversed(key.split("__")):
|
||||
key_parts = key.split("__")
|
||||
logger.info("Key parts: %s", key_parts)
|
||||
|
||||
# Check if any ccxt config key is in the key parts
|
||||
preserve_case = key_parts[0].lower() == "exchange" and any(
|
||||
ccxt_key in [part.lower() for part in key_parts] for ccxt_key in ccxt_config_keys
|
||||
)
|
||||
|
||||
for i, k in enumerate(reversed(key_parts)):
|
||||
# Preserve case for the final key if ccxt config is involved
|
||||
key_name = k if preserve_case and i == 0 else k.lower()
|
||||
|
||||
val = {
|
||||
k.lower(): (
|
||||
key_name: (
|
||||
_get_var_typed(val)
|
||||
if not isinstance(val, dict) and k not in no_convert
|
||||
else val
|
||||
|
||||
@@ -4,9 +4,8 @@ This module contains the argument manager class
|
||||
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from typing_extensions import Self
|
||||
from datetime import UTC, datetime
|
||||
from typing import Self
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.exceptions import ConfigurationError
|
||||
@@ -151,9 +150,7 @@ class TimeRange:
|
||||
starts = rvals[index]
|
||||
if stype[0] == "date" and len(starts) == 8:
|
||||
start = int(
|
||||
datetime.strptime(starts, "%Y%m%d")
|
||||
.replace(tzinfo=timezone.utc)
|
||||
.timestamp()
|
||||
datetime.strptime(starts, "%Y%m%d").replace(tzinfo=UTC).timestamp()
|
||||
)
|
||||
elif len(starts) == 13:
|
||||
start = int(starts) // 1000
|
||||
@@ -164,9 +161,7 @@ class TimeRange:
|
||||
stops = rvals[index]
|
||||
if stype[1] == "date" and len(stops) == 8:
|
||||
stop = int(
|
||||
datetime.strptime(stops, "%Y%m%d")
|
||||
.replace(tzinfo=timezone.utc)
|
||||
.timestamp()
|
||||
datetime.strptime(stops, "%Y%m%d").replace(tzinfo=UTC).timestamp()
|
||||
)
|
||||
elif len(stops) == 13:
|
||||
stop = int(stops) // 1000
|
||||
|
||||
@@ -5,7 +5,7 @@ Helpers when analyzing backtest data
|
||||
import logging
|
||||
import zipfile
|
||||
from copy import copy
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from io import BytesIO, StringIO
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
@@ -324,7 +324,7 @@ def find_existing_backtest_stats(
|
||||
|
||||
if min_backtest_date is not None:
|
||||
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:
|
||||
# Do not use a cached result for this strategy as first result is too old.
|
||||
del run_ids[strategy_name]
|
||||
|
||||
@@ -69,6 +69,10 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str):
|
||||
trades = pd.concat(dfs, ignore_index=True)
|
||||
del dfs
|
||||
|
||||
# drop any row not having a number in the column timestamp
|
||||
timestamp_numeric = pd.to_numeric(trades["timestamp"], errors="coerce")
|
||||
trades = trades[timestamp_numeric.notna()]
|
||||
|
||||
trades.loc[:, "timestamp"] = trades["timestamp"] * 1e3
|
||||
trades.loc[:, "cost"] = trades["price"] * trades["amount"]
|
||||
for col in DEFAULT_TRADES_COLUMNS:
|
||||
|
||||
@@ -7,7 +7,7 @@ Common Interface for bot and strategy to access data.
|
||||
|
||||
import logging
|
||||
from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
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!)
|
||||
"""
|
||||
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
|
||||
def _set_producer_pairs(self, pairlist: list[str], producer_name: str = "default"):
|
||||
@@ -131,7 +131,7 @@ class DataProvider:
|
||||
"data": {
|
||||
"key": pair_key,
|
||||
"df": dataframe.tail(1),
|
||||
"la": datetime.now(timezone.utc),
|
||||
"la": datetime.now(UTC),
|
||||
},
|
||||
}
|
||||
self.__rpc.send_msg(msg)
|
||||
@@ -164,7 +164,7 @@ class DataProvider:
|
||||
if producer_name not in self.__producer_pairs_df:
|
||||
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)
|
||||
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 producer_name not in self.__producer_pairs_df:
|
||||
# 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 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)
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
|
||||
|
||||
# We have it, return this data
|
||||
df, la = self.__producer_pairs_df[producer_name][pair_key]
|
||||
@@ -396,16 +396,16 @@ class DataProvider:
|
||||
if (max_index := self.__slice_index.get(pair)) is not None:
|
||||
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES) : max_index]
|
||||
else:
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
|
||||
return df, date
|
||||
else:
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
|
||||
|
||||
@property
|
||||
def runmode(self) -> RunMode:
|
||||
"""
|
||||
Get runmode of the bot
|
||||
can be "live", "dry-run", "backtest", "edgecli", "hyperopt" or "other".
|
||||
can be "live", "dry-run", "backtest", "hyperopt" or "other".
|
||||
"""
|
||||
return RunMode(self._config.get("runmode", RunMode.OTHER))
|
||||
|
||||
|
||||
@@ -331,7 +331,9 @@ def process_entry_exit_reasons(config: Config):
|
||||
exit_only = config.get("exit_only", False)
|
||||
do_rejected = config.get("analysis_rejected", False)
|
||||
to_csv = config.get("analysis_to_csv", False)
|
||||
csv_path = Path(config.get("analysis_csv_path", config["exportfilename"]))
|
||||
csv_path = Path(
|
||||
config.get("analysis_csv_path", config["exportfilename"]), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
if entry_only is True and exit_only is True:
|
||||
raise OperationalException(
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from pandas import DataFrame, to_datetime
|
||||
@@ -118,8 +118,8 @@ class IDataHandler(ABC):
|
||||
df = self._ohlcv_load(pair, timeframe, None, candle_type)
|
||||
if df.empty:
|
||||
return (
|
||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||
datetime.fromtimestamp(0, tz=UTC),
|
||||
datetime.fromtimestamp(0, tz=UTC),
|
||||
0,
|
||||
)
|
||||
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)
|
||||
if df.empty:
|
||||
return (
|
||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||
datetime.fromtimestamp(0, tz=UTC),
|
||||
datetime.fromtimestamp(0, tz=UTC),
|
||||
0,
|
||||
)
|
||||
return (
|
||||
|
||||
@@ -174,12 +174,18 @@ def calculate_underwater(
|
||||
|
||||
@dataclass()
|
||||
class DrawDownResult:
|
||||
# Max drawdown fields
|
||||
drawdown_abs: float = 0.0
|
||||
high_date: pd.Timestamp = None
|
||||
low_date: pd.Timestamp = None
|
||||
high_value: float = 0.0
|
||||
low_value: 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(
|
||||
@@ -191,29 +197,31 @@ def calculate_max_drawdown(
|
||||
relative: bool = False,
|
||||
) -> DrawDownResult:
|
||||
"""
|
||||
Calculate max drawdown and the corresponding close dates
|
||||
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
||||
Calculate max drawdown and current drawdown with corresponding dates
|
||||
: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 value_col: Column in DataFrame to use for values (defaults to 'profit_abs')
|
||||
: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
|
||||
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.
|
||||
"""
|
||||
if len(trades) == 0:
|
||||
raise ValueError("Trade dataframe empty.")
|
||||
|
||||
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
||||
max_drawdown_df = _calc_drawdown_series(
|
||||
profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance
|
||||
)
|
||||
|
||||
# Calculate maximum drawdown
|
||||
idxmin = (
|
||||
max_drawdown_df["drawdown_relative"].idxmax()
|
||||
if relative
|
||||
else max_drawdown_df["drawdown"].idxmin()
|
||||
)
|
||||
|
||||
high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax()
|
||||
high_date = profit_results.loc[high_idx, 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"]
|
||||
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(
|
||||
# Max drawdown
|
||||
drawdown_abs=abs(max_drawdown_df.loc[idxmin, "drawdown"]),
|
||||
high_date=high_date,
|
||||
low_date=low_date,
|
||||
high_value=high_val,
|
||||
low_value=low_val,
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from .edge_positioning import Edge, PairInfo # noqa: F401
|
||||
@@ -1,524 +0,0 @@
|
||||
# pragma pylint: disable=W0603
|
||||
"""Edge positioning package"""
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import numpy as np
|
||||
import utils_find_1st as utf1st
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT, Config
|
||||
from freqtrade.data.history import get_timerange, load_data, refresh_data
|
||||
from freqtrade.enums import CandleType, ExitType, RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.util import dt_now
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PairInfo(NamedTuple):
|
||||
stoploss: float
|
||||
winrate: float
|
||||
risk_reward_ratio: float
|
||||
required_risk_reward: float
|
||||
expectancy: float
|
||||
nb_trades: int
|
||||
avg_trade_duration: float
|
||||
|
||||
|
||||
class Edge:
|
||||
"""
|
||||
Calculates Win Rate, Risk Reward Ratio, Expectancy
|
||||
against historical data for a give set of markets and a strategy
|
||||
it then adjusts stoploss and position size accordingly
|
||||
and force it into the strategy
|
||||
Author: https://github.com/mishaker
|
||||
"""
|
||||
|
||||
_cached_pairs: dict[str, Any] = {} # Keeps a list of pairs
|
||||
|
||||
def __init__(self, config: Config, exchange, strategy) -> None:
|
||||
self.config = config
|
||||
self.exchange = exchange
|
||||
self.strategy: IStrategy = strategy
|
||||
|
||||
self.edge_config = self.config.get("edge", {})
|
||||
self._cached_pairs: dict[str, Any] = {} # Keeps a list of pairs
|
||||
self._final_pairs: list = []
|
||||
|
||||
# checking max_open_trades. it should be -1 as with Edge
|
||||
# the number of trades is determined by position size
|
||||
if self.config["max_open_trades"] != float("inf"):
|
||||
logger.critical("max_open_trades should be -1 in config !")
|
||||
|
||||
if self.config["stake_amount"] != UNLIMITED_STAKE_AMOUNT:
|
||||
raise OperationalException("Edge works only with unlimited stake amount")
|
||||
|
||||
self._capital_ratio: float = self.config["tradable_balance_ratio"]
|
||||
self._allowed_risk: float = self.edge_config.get("allowed_risk")
|
||||
self._since_number_of_days: int = self.edge_config.get("calculate_since_number_of_days", 14)
|
||||
self._last_updated: int = 0 # Timestamp of pairs last updated time
|
||||
self._refresh_pairs = True
|
||||
|
||||
self._stoploss_range_min = float(self.edge_config.get("stoploss_range_min", -0.01))
|
||||
self._stoploss_range_max = float(self.edge_config.get("stoploss_range_max", -0.05))
|
||||
self._stoploss_range_step = float(self.edge_config.get("stoploss_range_step", -0.001))
|
||||
|
||||
# calculating stoploss range
|
||||
self._stoploss_range = np.arange(
|
||||
self._stoploss_range_min, self._stoploss_range_max, self._stoploss_range_step
|
||||
)
|
||||
|
||||
self._timerange: TimeRange = TimeRange.parse_timerange(
|
||||
f"{(dt_now() - timedelta(days=self._since_number_of_days)).strftime('%Y%m%d')}-"
|
||||
)
|
||||
if config.get("fee"):
|
||||
self.fee = config["fee"]
|
||||
else:
|
||||
try:
|
||||
self.fee = self.exchange.get_fee(
|
||||
symbol=expand_pairlist(
|
||||
self.config["exchange"]["pair_whitelist"], list(self.exchange.markets)
|
||||
)[0]
|
||||
)
|
||||
except IndexError:
|
||||
self.fee = None
|
||||
|
||||
def calculate(self, pairs: list[str]) -> bool:
|
||||
if self.fee is None and pairs:
|
||||
self.fee = self.exchange.get_fee(pairs[0])
|
||||
|
||||
heartbeat = self.edge_config.get("process_throttle_secs")
|
||||
|
||||
if (self._last_updated > 0) and (
|
||||
self._last_updated + heartbeat > int(dt_now().timestamp())
|
||||
):
|
||||
return False
|
||||
|
||||
data: dict[str, Any] = {}
|
||||
logger.info("Using stake_currency: %s ...", self.config["stake_currency"])
|
||||
logger.info("Using local backtesting data (using whitelist in given config) ...")
|
||||
|
||||
if self._refresh_pairs:
|
||||
timerange_startup = deepcopy(self._timerange)
|
||||
timerange_startup.subtract_start(
|
||||
timeframe_to_seconds(self.strategy.timeframe) * self.strategy.startup_candle_count
|
||||
)
|
||||
refresh_data(
|
||||
datadir=self.config["datadir"],
|
||||
pairs=pairs,
|
||||
exchange=self.exchange,
|
||||
timeframe=self.strategy.timeframe,
|
||||
timerange=timerange_startup,
|
||||
data_format=self.config["dataformat_ohlcv"],
|
||||
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||
)
|
||||
# Download informative pairs too
|
||||
res = defaultdict(list)
|
||||
for pair, timeframe, _ in self.strategy.gather_informative_pairs():
|
||||
res[timeframe].append(pair)
|
||||
for timeframe, inf_pairs in res.items():
|
||||
timerange_startup = deepcopy(self._timerange)
|
||||
timerange_startup.subtract_start(
|
||||
timeframe_to_seconds(timeframe) * self.strategy.startup_candle_count
|
||||
)
|
||||
refresh_data(
|
||||
datadir=self.config["datadir"],
|
||||
pairs=inf_pairs,
|
||||
exchange=self.exchange,
|
||||
timeframe=timeframe,
|
||||
timerange=timerange_startup,
|
||||
data_format=self.config["dataformat_ohlcv"],
|
||||
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||
)
|
||||
|
||||
data = load_data(
|
||||
datadir=self.config["datadir"],
|
||||
pairs=pairs,
|
||||
timeframe=self.strategy.timeframe,
|
||||
timerange=self._timerange,
|
||||
startup_candles=self.strategy.startup_candle_count,
|
||||
data_format=self.config["dataformat_ohlcv"],
|
||||
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||
)
|
||||
|
||||
if not data:
|
||||
# Reinitializing cached pairs
|
||||
self._cached_pairs = {}
|
||||
logger.critical("No data found. Edge is stopped ...")
|
||||
return False
|
||||
# Fake run-mode to Edge
|
||||
prior_rm = self.config["runmode"]
|
||||
self.config["runmode"] = RunMode.EDGE
|
||||
preprocessed = self.strategy.advise_all_indicators(data)
|
||||
self.config["runmode"] = prior_rm
|
||||
|
||||
# Print timeframe
|
||||
min_date, max_date = get_timerange(preprocessed)
|
||||
logger.info(
|
||||
f"Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} "
|
||||
f"up to {max_date.strftime(DATETIME_PRINT_FORMAT)} "
|
||||
f"({(max_date - min_date).days} days).."
|
||||
)
|
||||
# TODO: Should edge support shorts? needs to be investigated further
|
||||
# * (add enter_short exit_short)
|
||||
headers = ["date", "open", "high", "low", "close", "enter_long", "exit_long"]
|
||||
|
||||
trades: list = []
|
||||
for pair, pair_data in preprocessed.items():
|
||||
# Sorting dataframe by date and reset index
|
||||
pair_data = pair_data.sort_values(by=["date"])
|
||||
pair_data = pair_data.reset_index(drop=True)
|
||||
|
||||
df_analyzed = self.strategy.ft_advise_signals(pair_data, {"pair": pair})[headers].copy()
|
||||
|
||||
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
|
||||
|
||||
# If no trade found then exit
|
||||
if len(trades) == 0:
|
||||
logger.info("No trades found.")
|
||||
return False
|
||||
|
||||
# Fill missing, calculable columns, profit, duration , abs etc.
|
||||
trades_df = self._fill_calculable_fields(DataFrame(trades))
|
||||
self._cached_pairs = self._process_expectancy(trades_df)
|
||||
self._last_updated = int(dt_now().timestamp())
|
||||
|
||||
return True
|
||||
|
||||
def stake_amount(
|
||||
self, pair: str, free_capital: float, total_capital: float, capital_in_trade: float
|
||||
) -> float:
|
||||
stoploss = self.get_stoploss(pair)
|
||||
available_capital = (total_capital + capital_in_trade) * self._capital_ratio
|
||||
allowed_capital_at_risk = available_capital * self._allowed_risk
|
||||
max_position_size = abs(allowed_capital_at_risk / stoploss)
|
||||
# Position size must be below available capital.
|
||||
position_size = min(min(max_position_size, free_capital), available_capital)
|
||||
if pair in self._cached_pairs:
|
||||
logger.info(
|
||||
"winrate: %s, expectancy: %s, position size: %s, pair: %s,"
|
||||
" capital in trade: %s, free capital: %s, total capital: %s,"
|
||||
" stoploss: %s, available capital: %s.",
|
||||
self._cached_pairs[pair].winrate,
|
||||
self._cached_pairs[pair].expectancy,
|
||||
position_size,
|
||||
pair,
|
||||
capital_in_trade,
|
||||
free_capital,
|
||||
total_capital,
|
||||
stoploss,
|
||||
available_capital,
|
||||
)
|
||||
return round(position_size, 15)
|
||||
|
||||
def get_stoploss(self, pair: str) -> float:
|
||||
if pair in self._cached_pairs:
|
||||
return self._cached_pairs[pair].stoploss
|
||||
else:
|
||||
logger.warning(
|
||||
f"Tried to access stoploss of non-existing pair {pair}, "
|
||||
"strategy stoploss is returned instead."
|
||||
)
|
||||
return self.strategy.stoploss
|
||||
|
||||
def adjust(self, pairs: list[str]) -> list:
|
||||
"""
|
||||
Filters out and sorts "pairs" according to Edge calculated pairs
|
||||
"""
|
||||
final = []
|
||||
for pair, info in self._cached_pairs.items():
|
||||
if (
|
||||
info.expectancy > float(self.edge_config.get("minimum_expectancy", 0.2))
|
||||
and info.winrate > float(self.edge_config.get("minimum_winrate", 0.60))
|
||||
and pair in pairs
|
||||
):
|
||||
final.append(pair)
|
||||
|
||||
if self._final_pairs != final:
|
||||
self._final_pairs = final
|
||||
if self._final_pairs:
|
||||
logger.info(
|
||||
"Minimum expectancy and minimum winrate are met only for %s,"
|
||||
" so other pairs are filtered out.",
|
||||
self._final_pairs,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"Edge removed all pairs as no pair with minimum expectancy "
|
||||
"and minimum winrate was found !"
|
||||
)
|
||||
|
||||
return self._final_pairs
|
||||
|
||||
def accepted_pairs(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
return a list of accepted pairs along with their winrate, expectancy and stoploss
|
||||
"""
|
||||
final = []
|
||||
for pair, info in self._cached_pairs.items():
|
||||
if info.expectancy > float(
|
||||
self.edge_config.get("minimum_expectancy", 0.2)
|
||||
) and info.winrate > float(self.edge_config.get("minimum_winrate", 0.60)):
|
||||
final.append(
|
||||
{
|
||||
"Pair": pair,
|
||||
"Winrate": info.winrate,
|
||||
"Expectancy": info.expectancy,
|
||||
"Stoploss": info.stoploss,
|
||||
}
|
||||
)
|
||||
return final
|
||||
|
||||
def _fill_calculable_fields(self, result: DataFrame) -> DataFrame:
|
||||
"""
|
||||
The result frame contains a number of columns that are calculable
|
||||
from other columns. These are left blank till all rows are added,
|
||||
to be populated in single vector calls.
|
||||
|
||||
Columns to be populated are:
|
||||
- Profit
|
||||
- trade duration
|
||||
- profit abs
|
||||
:param result Dataframe
|
||||
:return: result Dataframe
|
||||
"""
|
||||
# We set stake amount to an arbitrary amount, as it doesn't change the calculation.
|
||||
# All returned values are relative, they are defined as ratios.
|
||||
stake = 0.015
|
||||
|
||||
result["trade_duration"] = result["close_date"] - result["open_date"]
|
||||
|
||||
result["trade_duration"] = result["trade_duration"].map(
|
||||
lambda x: int(x.total_seconds() / 60)
|
||||
)
|
||||
|
||||
# Spends, Takes, Profit, Absolute Profit
|
||||
|
||||
# Buy Price
|
||||
result["buy_vol"] = stake / result["open_rate"] # How many target are we buying
|
||||
result["buy_fee"] = stake * self.fee
|
||||
result["buy_spend"] = stake + result["buy_fee"] # How much we're spending
|
||||
|
||||
# Sell price
|
||||
result["sell_sum"] = result["buy_vol"] * result["close_rate"]
|
||||
result["sell_fee"] = result["sell_sum"] * self.fee
|
||||
result["sell_take"] = result["sell_sum"] - result["sell_fee"]
|
||||
|
||||
# profit_ratio
|
||||
result["profit_ratio"] = (result["sell_take"] - result["buy_spend"]) / result["buy_spend"]
|
||||
|
||||
# Absolute profit
|
||||
result["profit_abs"] = result["sell_take"] - result["buy_spend"]
|
||||
|
||||
return result
|
||||
|
||||
def _process_expectancy(self, results: DataFrame) -> dict[str, Any]:
|
||||
"""
|
||||
This calculates WinRate, Required Risk Reward, Risk Reward and Expectancy of all pairs
|
||||
The calculation will be done per pair and per strategy.
|
||||
"""
|
||||
# Removing pairs having less than min_trades_number
|
||||
min_trades_number = self.edge_config.get("min_trade_number", 10)
|
||||
results = results.groupby(["pair", "stoploss"]).filter(lambda x: len(x) > min_trades_number)
|
||||
###################################
|
||||
|
||||
# Removing outliers (Only Pumps) from the dataset
|
||||
# The method to detect outliers is to calculate standard deviation
|
||||
# Then every value more than (standard deviation + 2*average) is out (pump)
|
||||
#
|
||||
# Removing Pumps
|
||||
if self.edge_config.get("remove_pumps", False):
|
||||
results = results[
|
||||
results["profit_abs"]
|
||||
< 2 * results["profit_abs"].std() + results["profit_abs"].mean()
|
||||
]
|
||||
##########################################################################
|
||||
|
||||
# Removing trades having a duration more than X minutes (set in config)
|
||||
max_trade_duration = self.edge_config.get("max_trade_duration_minute", 1440)
|
||||
results = results[results.trade_duration < max_trade_duration]
|
||||
#######################################################################
|
||||
|
||||
if results.empty:
|
||||
return {}
|
||||
|
||||
groupby_aggregator = {
|
||||
"profit_abs": [
|
||||
("nb_trades", "count"), # number of all trades
|
||||
("profit_sum", lambda x: x[x > 0].sum()), # cumulative profit of all winning trades
|
||||
("loss_sum", lambda x: abs(x[x < 0].sum())), # cumulative loss of all losing trades
|
||||
("nb_win_trades", lambda x: x[x > 0].count()), # number of winning trades
|
||||
],
|
||||
"trade_duration": [("avg_trade_duration", "mean")],
|
||||
}
|
||||
|
||||
# Group by (pair and stoploss) by applying above aggregator
|
||||
df = (
|
||||
results.groupby(["pair", "stoploss"])[["profit_abs", "trade_duration"]]
|
||||
.agg(groupby_aggregator)
|
||||
.reset_index(col_level=1)
|
||||
)
|
||||
|
||||
# Dropping level 0 as we don't need it
|
||||
df.columns = df.columns.droplevel(0)
|
||||
|
||||
# Calculating number of losing trades, average win and average loss
|
||||
df["nb_loss_trades"] = df["nb_trades"] - df["nb_win_trades"]
|
||||
df["average_win"] = np.where(
|
||||
df["nb_win_trades"] == 0, 0.0, df["profit_sum"] / df["nb_win_trades"]
|
||||
)
|
||||
df["average_loss"] = np.where(
|
||||
df["nb_loss_trades"] == 0, 0.0, df["loss_sum"] / df["nb_loss_trades"]
|
||||
)
|
||||
|
||||
# Win rate = number of profitable trades / number of trades
|
||||
df["winrate"] = df["nb_win_trades"] / df["nb_trades"]
|
||||
|
||||
# risk_reward_ratio = average win / average loss
|
||||
df["risk_reward_ratio"] = df["average_win"] / df["average_loss"]
|
||||
|
||||
# required_risk_reward = (1 / winrate) - 1
|
||||
df["required_risk_reward"] = (1 / df["winrate"]) - 1
|
||||
|
||||
# expectancy = (risk_reward_ratio * winrate) - (lossrate)
|
||||
df["expectancy"] = (df["risk_reward_ratio"] * df["winrate"]) - (1 - df["winrate"])
|
||||
|
||||
# sort by expectancy and stoploss
|
||||
df = (
|
||||
df.sort_values(by=["expectancy", "stoploss"], ascending=False)
|
||||
.groupby("pair")
|
||||
.first()
|
||||
.sort_values(by=["expectancy"], ascending=False)
|
||||
.reset_index()
|
||||
)
|
||||
|
||||
final = {}
|
||||
for x in df.itertuples():
|
||||
final[x.pair] = PairInfo(
|
||||
x.stoploss,
|
||||
x.winrate,
|
||||
x.risk_reward_ratio,
|
||||
x.required_risk_reward,
|
||||
x.expectancy,
|
||||
x.nb_trades,
|
||||
x.avg_trade_duration,
|
||||
)
|
||||
|
||||
# Returning a list of pairs in order of "expectancy"
|
||||
return final
|
||||
|
||||
def _find_trades_for_stoploss_range(self, df, pair: str, stoploss_range) -> list:
|
||||
buy_column = df["enter_long"].values
|
||||
sell_column = df["exit_long"].values
|
||||
date_column = df["date"].values
|
||||
ohlc_columns = df[["open", "high", "low", "close"]].values
|
||||
|
||||
result: list = []
|
||||
for stoploss in stoploss_range:
|
||||
result += self._detect_next_stop_or_sell_point(
|
||||
buy_column, sell_column, date_column, ohlc_columns, round(stoploss, 6), pair
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _detect_next_stop_or_sell_point(
|
||||
self, buy_column, sell_column, date_column, ohlc_columns, stoploss, pair: str
|
||||
):
|
||||
"""
|
||||
Iterate through ohlc_columns in order to find the next trade
|
||||
Next trade opens from the first buy signal noticed to
|
||||
The sell or stoploss signal after it.
|
||||
It then cuts OHLC, buy_column, sell_column and date_column.
|
||||
Cut from (the exit trade index) + 1.
|
||||
|
||||
Author: https://github.com/mishaker
|
||||
"""
|
||||
|
||||
result: list = []
|
||||
start_point = 0
|
||||
|
||||
while True:
|
||||
open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal)
|
||||
|
||||
# Return empty if we don't find trade entry (i.e. buy==1) or
|
||||
# we find a buy but at the end of array
|
||||
if open_trade_index == -1 or open_trade_index == len(buy_column) - 1:
|
||||
break
|
||||
else:
|
||||
# When a buy signal is seen,
|
||||
# trade opens in reality on the next candle
|
||||
open_trade_index += 1
|
||||
|
||||
open_price = ohlc_columns[open_trade_index, 0]
|
||||
stop_price = open_price * (stoploss + 1)
|
||||
|
||||
# Searching for the index where stoploss is hit
|
||||
stop_index = utf1st.find_1st(
|
||||
ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller
|
||||
)
|
||||
|
||||
# If we don't find it then we assume stop_index will be far in future (infinite number)
|
||||
if stop_index == -1:
|
||||
stop_index = float("inf")
|
||||
|
||||
# Searching for the index where sell is hit
|
||||
sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal)
|
||||
|
||||
# If we don't find it then we assume sell_index will be far in future (infinite number)
|
||||
if sell_index == -1:
|
||||
sell_index = float("inf")
|
||||
|
||||
# Check if we don't find any stop or sell point (in that case trade remains open)
|
||||
# It is not interesting for Edge to consider it so we simply ignore the trade
|
||||
# And stop iterating there is no more entry
|
||||
if stop_index == sell_index == float("inf"):
|
||||
break
|
||||
|
||||
if stop_index <= sell_index:
|
||||
exit_index = open_trade_index + stop_index
|
||||
exit_type = ExitType.STOP_LOSS
|
||||
exit_price = stop_price
|
||||
elif stop_index > sell_index:
|
||||
# If exit is SELL then we exit at the next candle
|
||||
exit_index = open_trade_index + sell_index + 1
|
||||
|
||||
# Check if we have the next candle
|
||||
if len(ohlc_columns) - 1 < exit_index:
|
||||
break
|
||||
|
||||
exit_type = ExitType.EXIT_SIGNAL
|
||||
exit_price = ohlc_columns[exit_index, 0]
|
||||
|
||||
trade = {
|
||||
"pair": pair,
|
||||
"stoploss": stoploss,
|
||||
"profit_ratio": "",
|
||||
"profit_abs": "",
|
||||
"open_date": date_column[open_trade_index],
|
||||
"close_date": date_column[exit_index],
|
||||
"trade_duration": "",
|
||||
"open_rate": round(open_price, 15),
|
||||
"close_rate": round(exit_price, 15),
|
||||
"exit_type": exit_type,
|
||||
}
|
||||
|
||||
result.append(trade)
|
||||
|
||||
# Giving a view of exit_index till the end of array
|
||||
buy_column = buy_column[exit_index:]
|
||||
sell_column = sell_column[exit_index:]
|
||||
date_column = date_column[exit_index:]
|
||||
ohlc_columns = ohlc_columns[exit_index:]
|
||||
start_point += exit_index
|
||||
|
||||
return result
|
||||
@@ -13,4 +13,4 @@ class MarginMode(str, Enum):
|
||||
NONE = ""
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
return f"{self.value.lower()}"
|
||||
|
||||
@@ -4,13 +4,12 @@ from enum import Enum
|
||||
class RunMode(str, Enum):
|
||||
"""
|
||||
Bot running mode (backtest, hyperopt, ...)
|
||||
can be "live", "dry-run", "backtest", "edge", "hyperopt".
|
||||
can be "live", "dry-run", "backtest", "hyperopt".
|
||||
"""
|
||||
|
||||
LIVE = "live"
|
||||
DRY_RUN = "dry_run"
|
||||
BACKTEST = "backtest"
|
||||
EDGE = "edge"
|
||||
HYPEROPT = "hyperopt"
|
||||
UTIL_EXCHANGE = "util_exchange"
|
||||
UTIL_NO_EXCHANGE = "util_no_exchange"
|
||||
@@ -20,5 +19,5 @@ class RunMode(str, Enum):
|
||||
|
||||
|
||||
TRADE_MODES = [RunMode.LIVE, RunMode.DRY_RUN]
|
||||
OPTIMIZE_MODES = [RunMode.BACKTEST, RunMode.EDGE, RunMode.HYPEROPT]
|
||||
OPTIMIZE_MODES = [RunMode.BACKTEST, RunMode.HYPEROPT]
|
||||
NON_UTIL_MODES = TRADE_MODES + OPTIMIZE_MODES
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# flake8: noqa: F401
|
||||
# isort: off
|
||||
from freqtrade.exchange.common import remove_exchange_credentials, MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.exchange.exchange import Exchange
|
||||
|
||||
# isort: on
|
||||
@@ -43,4 +43,6 @@ from freqtrade.exchange.idex import Idex
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
from freqtrade.exchange.kucoin import Kucoin
|
||||
from freqtrade.exchange.lbank import Lbank
|
||||
from freqtrade.exchange.luno import Luno
|
||||
from freqtrade.exchange.modetrade import Modetrade
|
||||
from freqtrade.exchange.okx import Okx
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Binance exchange subclass"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
import ccxt
|
||||
@@ -63,7 +63,7 @@ class Binance(Exchange):
|
||||
}
|
||||
|
||||
_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.FUTURES, MarginMode.CROSS),
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||
@@ -76,7 +76,10 @@ class Binance(Exchange):
|
||||
:return: Proxy coin or stake currency
|
||||
"""
|
||||
if self.margin_mode == MarginMode.CROSS:
|
||||
return self._config.get("proxy_coin", self._config["stake_currency"])
|
||||
return self._config.get(
|
||||
"proxy_coin",
|
||||
self._config["stake_currency"],
|
||||
) # type: ignore[return-value]
|
||||
return self._config["stake_currency"]
|
||||
|
||||
def get_tickers(
|
||||
@@ -157,7 +160,7 @@ class Binance(Exchange):
|
||||
since_ms = x[3][0][0]
|
||||
logger.info(
|
||||
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:
|
||||
logger.warning(
|
||||
@@ -396,7 +399,7 @@ class Binance(Exchange):
|
||||
trades = await self._api_async.fetch_trades(
|
||||
pair,
|
||||
params={
|
||||
self._trades_pagination_arg: "0",
|
||||
self._ft_has["trades_pagination_arg"]: "0",
|
||||
},
|
||||
limit=5,
|
||||
)
|
||||
|
||||
+23808
-16108
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
"""Bitpanda exchange subclass"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
@@ -34,5 +34,5 @@ class Bitpanda(Exchange):
|
||||
:param pair: Pair the order is for
|
||||
: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)
|
||||
|
||||
+31
-18
@@ -1,8 +1,5 @@
|
||||
"""Bybit exchange subclass"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import ccxt
|
||||
|
||||
@@ -12,6 +9,7 @@ from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalExcep
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -64,9 +62,9 @@ class Bybit(Exchange):
|
||||
}
|
||||
|
||||
_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.ISOLATED)
|
||||
]
|
||||
|
||||
@property
|
||||
@@ -76,14 +74,11 @@ class Bybit(Exchange):
|
||||
config = {}
|
||||
if self.trading_mode == TradingMode.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
|
||||
|
||||
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
|
||||
def additional_exchange_init(self) -> None:
|
||||
"""
|
||||
@@ -182,18 +177,36 @@ class Bybit(Exchange):
|
||||
PERPETUAL:
|
||||
bybit:
|
||||
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
|
||||
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
|
||||
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 = (
|
||||
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
- (Extra Margin Added/Contract Quantity))
|
||||
Short:
|
||||
Position Entry Price - [
|
||||
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
|
||||
]
|
||||
)
|
||||
|
||||
Short USDC:
|
||||
Liquidation Price = (
|
||||
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
+ (Extra Margin Added/Contract Quantity))
|
||||
Position Entry Price + [
|
||||
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
|
||||
]
|
||||
)
|
||||
|
||||
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 open_rate: Entry price of position
|
||||
|
||||
@@ -5,7 +5,6 @@ from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import Any, TypeVar, cast, overload
|
||||
|
||||
from freqtrade.constants import ExchangeConfig
|
||||
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
|
||||
@@ -104,20 +103,6 @@ EXCHANGE_HAS_OPTIONAL = [
|
||||
]
|
||||
|
||||
|
||||
def remove_exchange_credentials(exchange_config: ExchangeConfig, dry_run: bool) -> None:
|
||||
"""
|
||||
Removes exchange keys from the configuration and specifies dry-run
|
||||
Used for backtesting / hyperopt / edge and utils.
|
||||
Modifies the input dict!
|
||||
"""
|
||||
if dry_run:
|
||||
exchange_config["key"] = ""
|
||||
exchange_config["apiKey"] = ""
|
||||
exchange_config["secret"] = ""
|
||||
exchange_config["password"] = ""
|
||||
exchange_config["uid"] = ""
|
||||
|
||||
|
||||
def calculate_backoff(retrycount, max_retries):
|
||||
"""
|
||||
Calculate backoff
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
import signal
|
||||
from collections.abc import Coroutine, Generator
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from math import floor, isnan
|
||||
from threading import Lock
|
||||
from typing import Any, Literal, TypeGuard, TypeVar
|
||||
@@ -21,6 +21,7 @@ from ccxt import TICK_SIZE
|
||||
from dateutil import parser
|
||||
from pandas import DataFrame, concat
|
||||
|
||||
from freqtrade.configuration import remove_exchange_credentials
|
||||
from freqtrade.constants import (
|
||||
DEFAULT_AMOUNT_RESERVE_PERCENT,
|
||||
DEFAULT_TRADES_COLUMNS,
|
||||
@@ -64,7 +65,6 @@ from freqtrade.exceptions import (
|
||||
)
|
||||
from freqtrade.exchange.common import (
|
||||
API_FETCH_ORDER_RETRY_COUNT,
|
||||
remove_exchange_credentials,
|
||||
retrier,
|
||||
retrier_async,
|
||||
)
|
||||
@@ -137,6 +137,7 @@ class Exchange:
|
||||
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
||||
"ohlcv_partial_candle": True,
|
||||
"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
|
||||
"ohlcv_volume_currency": "base", # "base" or "quote"
|
||||
"tickers_have_quoteVolume": True,
|
||||
@@ -168,7 +169,8 @@ class Exchange:
|
||||
_ft_has_futures: FtHas = {}
|
||||
|
||||
_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__(
|
||||
@@ -197,7 +199,26 @@ class Exchange:
|
||||
self.loop = self._init_async_loop()
|
||||
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.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
|
||||
self._pairs_last_refresh_time: dict[PairWithTimeframe, int] = {}
|
||||
@@ -227,33 +248,17 @@ class Exchange:
|
||||
if config["dry_run"]:
|
||||
logger.info("Instance is running with dry_run enabled")
|
||||
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
|
||||
self.trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT)
|
||||
self.margin_mode: MarginMode = (
|
||||
MarginMode(config.get("margin_mode")) if config.get("margin_mode") else MarginMode.NONE
|
||||
# Don't remove exchange credentials for dry-run or if always_require_api_keys is set
|
||||
remove_exchange_credentials(
|
||||
exchange_conf,
|
||||
not self._ft_has["always_require_api_keys"] and config.get("dry_run", False),
|
||||
)
|
||||
self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
|
||||
|
||||
# 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)
|
||||
self.log_responses = exchange_conf.get("log_responses", False)
|
||||
|
||||
# Assign this directly for easy access
|
||||
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
|
||||
ccxt_config = self._ccxt_config
|
||||
ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_config", {}), ccxt_config)
|
||||
@@ -289,10 +294,6 @@ class Exchange:
|
||||
# Initial markets load
|
||||
self.reload_markets(True, load_leverage_tiers=False)
|
||||
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:
|
||||
self.fill_leverage_tiers()
|
||||
@@ -331,6 +332,12 @@ class Exchange:
|
||||
asyncio.set_event_loop(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:
|
||||
# Check if timeframe is available
|
||||
self.validate_timeframes(config.get("timeframe"))
|
||||
@@ -345,6 +352,8 @@ class Exchange:
|
||||
self.validate_orderflow(config["exchange"])
|
||||
self.validate_freqai(config)
|
||||
|
||||
self._set_startup_candle_count(config)
|
||||
|
||||
def _init_ccxt(
|
||||
self, exchange_config: dict[str, Any], sync: bool, ccxt_kwargs: dict[str, Any]
|
||||
) -> ccxt.Exchange:
|
||||
@@ -637,9 +646,9 @@ class Exchange:
|
||||
if self._exchange_ws:
|
||||
self._exchange_ws.reset_connections()
|
||||
|
||||
async def _api_reload_markets(self, reload: bool = False) -> dict[str, Any]:
|
||||
async def _api_reload_markets(self, reload: bool = False) -> None:
|
||||
try:
|
||||
return await self._api_async.load_markets(reload=reload, params={})
|
||||
await self._api_async.load_markets(reload=reload, params={})
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||
@@ -649,15 +658,15 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise TemporaryError(e) from e
|
||||
|
||||
def _load_async_markets(self, reload: bool = False) -> dict[str, Any]:
|
||||
def _load_async_markets(self, reload: bool = False) -> None:
|
||||
try:
|
||||
with self._loop_lock:
|
||||
markets = self.loop.run_until_complete(self._api_reload_markets(reload=reload))
|
||||
|
||||
if isinstance(markets, Exception):
|
||||
raise markets
|
||||
return markets
|
||||
except asyncio.TimeoutError as e:
|
||||
return None
|
||||
except TimeoutError as e:
|
||||
logger.warning("Could not load markets. Reason: %s", e)
|
||||
raise TemporaryError from e
|
||||
|
||||
@@ -679,7 +688,8 @@ class Exchange:
|
||||
# on initial load, we retry 3 times to ensure we get the markets
|
||||
retries: int = 3 if force else 0
|
||||
# Reload async markets, then assign them to sync api
|
||||
self._markets = retrier(self._load_async_markets, retries=retries)(reload=True)
|
||||
retrier(self._load_async_markets, retries=retries)(reload=True)
|
||||
self._markets = self._api_async.markets
|
||||
self._api.set_markets(self._api_async.markets, self._api_async.currencies)
|
||||
# Assign options array, as it contains some temporary information from the exchange.
|
||||
self._api.options = self._api_async.options
|
||||
@@ -876,10 +886,24 @@ class Exchange:
|
||||
(trading_mode, margin_mode) not in self._supported_trading_mode_margin_pairs
|
||||
):
|
||||
mm_value = margin_mode and margin_mode.value
|
||||
raise OperationalException(
|
||||
f"Freqtrade does not support {mm_value} {trading_mode} on {self.name}"
|
||||
raise ConfigurationError(
|
||||
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:
|
||||
"""
|
||||
Get parameter value from _ft_has
|
||||
@@ -2207,7 +2231,7 @@ class Exchange:
|
||||
_params = params if params else {}
|
||||
my_trades = self._api.fetch_my_trades(
|
||||
pair,
|
||||
int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000),
|
||||
int((since.replace(tzinfo=UTC).timestamp() - 5) * 1000),
|
||||
params=_params,
|
||||
)
|
||||
matched_trades = [trade for trade in my_trades if trade["order"] == order_id]
|
||||
@@ -2583,10 +2607,12 @@ class Exchange:
|
||||
if ticks and cache:
|
||||
idx = -2 if drop_incomplete and len(ticks) > 1 else -1
|
||||
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(
|
||||
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 (pair, timeframe, c_type) in self._klines:
|
||||
old = self._klines[(pair, timeframe, c_type)]
|
||||
@@ -2994,7 +3020,7 @@ class Exchange:
|
||||
returns: List of dicts containing trades, the next iteration value (new "since" or trade_id)
|
||||
"""
|
||||
try:
|
||||
trades_limit = self._max_trades_limit
|
||||
trades_limit = self._ft_has["trades_limit"]
|
||||
# fetch trades asynchronously
|
||||
if params:
|
||||
logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
|
||||
@@ -3038,7 +3064,7 @@ class Exchange:
|
||||
"""
|
||||
if not trades:
|
||||
return None
|
||||
if self._trades_pagination == "id":
|
||||
if self._ft_has["trades_pagination"] == "id":
|
||||
return trades[-1].get("id")
|
||||
else:
|
||||
return trades[-1].get("timestamp")
|
||||
@@ -3056,7 +3082,7 @@ class Exchange:
|
||||
) -> tuple[str, list[list]]:
|
||||
"""
|
||||
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 since: Since as integer timestamp in milliseconds
|
||||
:param until: Until as integer timestamp in milliseconds
|
||||
@@ -3082,7 +3108,7 @@ class Exchange:
|
||||
while True:
|
||||
try:
|
||||
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:
|
||||
trades.extend(t[x])
|
||||
@@ -3110,7 +3136,7 @@ class Exchange:
|
||||
) -> tuple[str, list[list]]:
|
||||
"""
|
||||
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 since: Since as integer timestamp in milliseconds
|
||||
:param until: Until as integer timestamp in milliseconds
|
||||
@@ -3164,9 +3190,9 @@ class Exchange:
|
||||
until = ccxt.Exchange.milliseconds()
|
||||
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)
|
||||
elif self._trades_pagination == "id":
|
||||
elif self._ft_has["trades_pagination"] == "id":
|
||||
return await self._async_get_trade_history_id(
|
||||
pair=pair, since=since, until=until, from_id=from_id
|
||||
)
|
||||
@@ -3334,7 +3360,7 @@ class Exchange:
|
||||
if not filename.parent.is_dir():
|
||||
filename.parent.mkdir(parents=True)
|
||||
data = {
|
||||
"updated": datetime.now(timezone.utc),
|
||||
"updated": datetime.now(UTC),
|
||||
"data": tiers,
|
||||
}
|
||||
file_dump_json(filename, data)
|
||||
@@ -3356,7 +3382,7 @@ class Exchange:
|
||||
updated = tiers.get("updated")
|
||||
if 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.")
|
||||
return None
|
||||
return tiers.get("data")
|
||||
@@ -3415,20 +3441,30 @@ class Exchange:
|
||||
# Find the appropriate tier based on stake_amount
|
||||
prior_max_lev = None
|
||||
for tier in pair_tiers:
|
||||
# Adjust notional by leverage to do a proper comparison
|
||||
min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"])
|
||||
max_stake = tier["maxNotional"] / tier["maxLeverage"]
|
||||
prior_max_lev = tier["maxLeverage"]
|
||||
# Adjust notional by leverage to do a proper comparison
|
||||
if min_stake <= stake_amount <= max_stake:
|
||||
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
|
||||
if stake_amount > max_stake:
|
||||
# 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(
|
||||
"Looped through all tiers without finding a max leverage. Should never be reached"
|
||||
f"Looped through all tiers without finding a max leverage for {pair}. "
|
||||
"Should never be reached."
|
||||
)
|
||||
|
||||
elif self.trading_mode == TradingMode.MARGIN: # Search markets.limits for max lev
|
||||
@@ -3570,7 +3606,7 @@ class Exchange:
|
||||
mark_price_type = CandleType.from_string(self._ft_has["mark_ohlcv_price"])
|
||||
|
||||
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))
|
||||
|
||||
mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type)
|
||||
|
||||
@@ -24,6 +24,7 @@ class FtHas(TypedDict, total=False):
|
||||
ohlcv_require_since: bool
|
||||
ohlcv_volume_currency: str
|
||||
ohlcv_candle_limit_per_timeframe: dict[str, int]
|
||||
always_require_api_keys: bool
|
||||
# Tickers
|
||||
tickers_have_quoteVolume: bool
|
||||
tickers_have_percentage: bool
|
||||
|
||||
@@ -3,7 +3,7 @@ Exchange support utils
|
||||
"""
|
||||
|
||||
import inspect
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from math import ceil, floor, isnan
|
||||
from typing import Any
|
||||
|
||||
@@ -27,7 +27,7 @@ from freqtrade.exchange.common import (
|
||||
SUPPORTED_EXCHANGES,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ def _build_exchange_list_entry(
|
||||
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
|
||||
}
|
||||
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}
|
||||
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:
|
||||
date = datetime.now(timezone.utc)
|
||||
date = datetime.now(UTC)
|
||||
|
||||
tf_min = timeframe_to_minutes(timeframe)
|
||||
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import ccxt
|
||||
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)
|
||||
"""
|
||||
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
|
||||
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)
|
||||
"""
|
||||
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
|
||||
return dt_from_ts(new_timestamp)
|
||||
|
||||
@@ -55,10 +55,10 @@ class Gate(Exchange):
|
||||
}
|
||||
|
||||
_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.FUTURES, MarginMode.CROSS),
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||
]
|
||||
|
||||
@retrier
|
||||
@@ -70,7 +70,6 @@ class Gate(Exchange):
|
||||
"""
|
||||
try:
|
||||
if not self._config["dry_run"]:
|
||||
# TODO: This should work with 4.4.34 and later.
|
||||
self._api.load_unified_status()
|
||||
is_unified = self._api.options.get("unifiedAccount")
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ class Hyperliquid(Exchange):
|
||||
"stoploss_on_exchange": False,
|
||||
"exchange_has_overrides": {"fetchTrades": False},
|
||||
"marketOrderRequiresPrice": True,
|
||||
"ws_enabled": True,
|
||||
}
|
||||
_ft_has_futures: FtHas = {
|
||||
"stoploss_on_exchange": True,
|
||||
@@ -40,7 +41,8 @@ class Hyperliquid(Exchange):
|
||||
}
|
||||
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||
(TradingMode.SPOT, MarginMode.NONE),
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||
]
|
||||
|
||||
@property
|
||||
|
||||
@@ -35,7 +35,7 @@ class Kraken(Exchange):
|
||||
}
|
||||
|
||||
_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.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]] = [
|
||||
# TradingMode.SPOT always supported and not required in this list
|
||||
(TradingMode.SPOT, MarginMode.NONE),
|
||||
# (TradingMode.MARGIN, MarginMode.CROSS),
|
||||
# (TradingMode.FUTURES, MarginMode.CROSS),
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||
|
||||
@@ -3,7 +3,7 @@ import importlib
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -239,7 +239,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
||||
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)
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
if trade.is_short:
|
||||
|
||||
@@ -5,7 +5,7 @@ import re
|
||||
import shutil
|
||||
import threading
|
||||
import warnings
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, TypedDict
|
||||
|
||||
@@ -116,7 +116,7 @@ class FreqaiDataDrawer:
|
||||
if metric not in self.metric_tracker[pair]:
|
||||
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]["timestamp"].append(timestamp)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
import logging
|
||||
import random
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -341,7 +341,7 @@ class FreqaiDataKitchen:
|
||||
full_timerange = TimeRange.parse_timerange(tr)
|
||||
config_timerange = TimeRange.parse_timerange(self.config["timerange"])
|
||||
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_backtest = copy.deepcopy(full_timerange)
|
||||
|
||||
@@ -525,7 +525,7 @@ class FreqaiDataKitchen:
|
||||
:return:
|
||||
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
|
||||
max_time = self.freqai_config.get("expiration_hours", 0)
|
||||
if max_time > 0:
|
||||
@@ -536,7 +536,7 @@ class FreqaiDataKitchen:
|
||||
def check_if_new_training_required(
|
||||
self, trained_timestamp: int
|
||||
) -> tuple[bool, TimeRange, TimeRange]:
|
||||
time = datetime.now(tz=timezone.utc).timestamp()
|
||||
time = datetime.now(tz=UTC).timestamp()
|
||||
trained_timerange = TimeRange()
|
||||
data_load_timerange = TimeRange()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import threading
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
@@ -76,7 +76,7 @@ class IFreqaiModel(ABC):
|
||||
|
||||
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config)
|
||||
# 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.scanning = False
|
||||
self.ft_params = self.freqai_info["feature_parameters"]
|
||||
|
||||
@@ -101,7 +101,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
|
||||
|
||||
return model
|
||||
|
||||
MyRLEnv: type[BaseEnvironment]
|
||||
MyRLEnv: type[BaseEnvironment] # type: ignore[assignment, unused-ignore]
|
||||
|
||||
class MyRLEnv(Base5ActionRLEnv): # type: ignore[no-redef]
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
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
|
||||
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")
|
||||
|
||||
|
||||
+94
-117
@@ -5,7 +5,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
|
||||
import logging
|
||||
import traceback
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, time, timedelta, timezone
|
||||
from datetime import UTC, datetime, time, timedelta
|
||||
from math import isclose
|
||||
from threading import Lock
|
||||
from time import sleep
|
||||
@@ -14,11 +14,10 @@ from typing import Any
|
||||
from schedule import Scheduler
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration import validate_config_consistency
|
||||
from freqtrade.configuration import remove_exchange_credentials, validate_config_consistency
|
||||
from freqtrade.constants import BuySell, Config, EntryExecuteMode, ExchangeConfig, LongShort
|
||||
from freqtrade.data.converter import order_book_to_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.edge import Edge
|
||||
from freqtrade.enums import (
|
||||
ExitCheckTuple,
|
||||
ExitType,
|
||||
@@ -38,7 +37,6 @@ from freqtrade.exceptions import (
|
||||
from freqtrade.exchange import (
|
||||
ROUND_DOWN,
|
||||
ROUND_UP,
|
||||
remove_exchange_credentials,
|
||||
timeframe_to_minutes,
|
||||
timeframe_to_next_date,
|
||||
timeframe_to_seconds,
|
||||
@@ -95,14 +93,16 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Remove credentials from original exchange config to avoid accidental credential exposure
|
||||
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)
|
||||
|
||||
# Check config consistency here since strategies can set certain options
|
||||
validate_config_consistency(config)
|
||||
|
||||
self.exchange = ExchangeResolver.load_exchange(
|
||||
self.config, exchange_config=exchange_config, load_leverage_tiers=True
|
||||
)
|
||||
# Re-validate exchange compatibility
|
||||
self.exchange.validate_config(self.config)
|
||||
|
||||
init_db(self.config["db_url"])
|
||||
|
||||
@@ -131,13 +131,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Attach Wallets to strategy instance
|
||||
self.strategy.wallets = self.wallets
|
||||
|
||||
# Initializing Edge only if enabled
|
||||
self.edge = (
|
||||
Edge(self.config, self.exchange, self.strategy)
|
||||
if self.config.get("edge", {}).get("enabled", False)
|
||||
else None
|
||||
)
|
||||
|
||||
# Init ExternalMessageConsumer if enabled
|
||||
self.emc = (
|
||||
ExternalMessageConsumer(self.config, self.dataprovider)
|
||||
@@ -242,9 +235,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
|
||||
# Update older trades with precision and precision mode
|
||||
self.startup_backpopulate_precision()
|
||||
if not self.edge:
|
||||
# Adjust stoploss if it was changed
|
||||
Trade.stoploss_reinitialization(self.strategy.stoploss)
|
||||
# Adjust stoploss if it was changed
|
||||
Trade.stoploss_reinitialization(self.strategy.stoploss)
|
||||
|
||||
# Only update open orders on startup
|
||||
# This will update the database after the initial migration
|
||||
@@ -276,7 +268,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -306,7 +298,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self._schedule.run_pending()
|
||||
Trade.commit()
|
||||
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:
|
||||
"""
|
||||
@@ -335,7 +327,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
def _refresh_active_whitelist(self, trades: list[Trade] | None = None) -> list[str]:
|
||||
"""
|
||||
Refresh active whitelist from pairlist or edge and extend it with
|
||||
Refresh active whitelist from pairlist and extend it with
|
||||
pairs that have open trades.
|
||||
"""
|
||||
# Refresh whitelist
|
||||
@@ -343,11 +335,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.pairlists.refresh_pairlist()
|
||||
_whitelist = self.pairlists.whitelist
|
||||
|
||||
# Calculating Edge positioning
|
||||
if self.edge:
|
||||
self.edge.calculate(_whitelist)
|
||||
_whitelist = self.edge.adjust(_whitelist)
|
||||
|
||||
if trades:
|
||||
# Extend active-pair whitelist with pairs of open trades
|
||||
# It ensures that candle (OHLCV) data are downloaded for open trades as well
|
||||
@@ -436,7 +423,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
except InvalidOrderException as 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(
|
||||
"Order is older than 5 days. Assuming order was fully cancelled."
|
||||
)
|
||||
@@ -701,9 +688,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
else:
|
||||
self.log_once(f"Pair {pair} is currently locked.", logger.info)
|
||||
return False
|
||||
stake_amount = self.wallets.get_trade_stake_amount(
|
||||
pair, self.config["max_open_trades"], self.edge
|
||||
)
|
||||
stake_amount = self.wallets.get_trade_stake_amount(pair, self.config["max_open_trades"])
|
||||
|
||||
bid_check_dom = self.config.get("entry_pricing", {}).get("check_depth_of_market", {})
|
||||
if (bid_check_dom.get("enabled", False)) and (
|
||||
@@ -772,7 +757,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
|
||||
stake_amount, order_tag = self.strategy._adjust_trade_position_internal(
|
||||
trade=trade,
|
||||
current_time=datetime.now(timezone.utc),
|
||||
current_time=datetime.now(UTC),
|
||||
current_rate=current_entry_rate,
|
||||
current_profit=current_entry_profit,
|
||||
min_stake=min_entry_stake,
|
||||
@@ -933,7 +918,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
amount=amount,
|
||||
rate=enter_limit_requested,
|
||||
time_in_force=time_in_force,
|
||||
current_time=datetime.now(timezone.utc),
|
||||
current_time=datetime.now(UTC),
|
||||
entry_tag=enter_tag,
|
||||
side=trade_side,
|
||||
):
|
||||
@@ -1004,7 +989,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker="maker")
|
||||
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(
|
||||
pair=pair,
|
||||
@@ -1042,13 +1027,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
precision_mode_price=self.exchange.precision_mode_price,
|
||||
contract_size=self.exchange.get_contract_size(pair),
|
||||
)
|
||||
stoploss = self.strategy.stoploss if not self.edge else self.edge.get_stoploss(pair)
|
||||
stoploss = self.strategy.stoploss
|
||||
trade.adjust_stop_loss(trade.open_rate, stoploss, initial=True)
|
||||
|
||||
else:
|
||||
# This is additional entry, we reset fee_open_currency so timeout checking can work
|
||||
trade.is_open = True
|
||||
trade.fee_open_currency = None
|
||||
trade.set_funding_fees(funding_fees)
|
||||
|
||||
trade.orders.append(order_obj)
|
||||
@@ -1125,7 +1108,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
)(
|
||||
pair=pair,
|
||||
trade=trade,
|
||||
current_time=datetime.now(timezone.utc),
|
||||
current_time=datetime.now(UTC),
|
||||
proposed_rate=enter_limit_requested,
|
||||
entry_tag=entry_tag,
|
||||
side=trade_side,
|
||||
@@ -1143,7 +1126,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
else:
|
||||
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
|
||||
pair=pair,
|
||||
current_time=datetime.now(timezone.utc),
|
||||
current_time=datetime.now(UTC),
|
||||
current_rate=enter_limit_requested,
|
||||
proposed_leverage=1.0,
|
||||
max_leverage=max_leverage,
|
||||
@@ -1170,13 +1153,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
pair, enter_limit_requested, leverage
|
||||
)
|
||||
|
||||
if not self.edge and trade is None:
|
||||
if trade is None:
|
||||
stake_available = self.wallets.get_available_stake_amount()
|
||||
stake_amount = strategy_safe_wrapper(
|
||||
self.strategy.custom_stake_amount, default_retval=stake_amount
|
||||
)(
|
||||
pair=pair,
|
||||
current_time=datetime.now(timezone.utc),
|
||||
current_time=datetime.now(UTC),
|
||||
current_rate=enter_limit_requested,
|
||||
proposed_stake=stake_amount,
|
||||
min_stake=min_stake_amount,
|
||||
@@ -1233,6 +1216,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"leverage": trade.leverage if trade.leverage else None,
|
||||
"direction": "Short" if trade.is_short else "Long",
|
||||
"limit": open_rate, # Deprecated (?)
|
||||
"order_rate": open_rate,
|
||||
"open_rate": open_rate,
|
||||
"order_type": order_type or "unknown",
|
||||
"stake_amount": stake_amount,
|
||||
@@ -1241,7 +1225,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"quote_currency": self.exchange.get_pair_quote_currency(trade.pair),
|
||||
"fiat_currency": self.config.get("fiat_display_currency", None),
|
||||
"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,
|
||||
"sub_trade": sub_trade,
|
||||
}
|
||||
@@ -1269,6 +1253,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"leverage": trade.leverage,
|
||||
"direction": "Short" if trade.is_short else "Long",
|
||||
"limit": trade.open_rate,
|
||||
"order_rate": trade.open_rate,
|
||||
"order_type": order_type,
|
||||
"stake_amount": trade.stake_amount,
|
||||
"open_rate": trade.open_rate,
|
||||
@@ -1299,6 +1284,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if (
|
||||
not trade.has_open_orders
|
||||
and not trade.has_open_sl_orders
|
||||
and trade.fee_open_currency is not None
|
||||
and not self.wallets.check_exit_amount(trade)
|
||||
):
|
||||
logger.warning(
|
||||
@@ -1379,10 +1365,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
exits: list[ExitCheckTuple] = self.strategy.should_exit(
|
||||
trade,
|
||||
exit_rate,
|
||||
datetime.now(timezone.utc),
|
||||
datetime.now(UTC),
|
||||
enter=enter,
|
||||
exit_=exit_,
|
||||
force_stoploss=self.edge.get_stoploss(trade.pair) if self.edge else 0,
|
||||
force_stoploss=0,
|
||||
)
|
||||
for should_exit in exits:
|
||||
if should_exit.exit_flag:
|
||||
@@ -1487,13 +1473,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
# If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
||||
if len(stoploss_orders) == 0:
|
||||
stop_price = trade.stoploss_or_liquidation
|
||||
if self.edge:
|
||||
stoploss = self.edge.get_stoploss(pair=trade.pair)
|
||||
stop_price = (
|
||||
trade.open_rate * (1 - stoploss)
|
||||
if trade.is_short
|
||||
else trade.open_rate * (1 + stoploss)
|
||||
)
|
||||
|
||||
if self.create_stoploss_order(trade=trade, stop_price=stop_price):
|
||||
# The above will return False if the placement failed and the trade was force-sold.
|
||||
@@ -1504,44 +1483,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
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]):
|
||||
"""
|
||||
Perform required actions according to existing stoploss orders of trade
|
||||
@@ -1583,6 +1524,44 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
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:
|
||||
"""
|
||||
Management of open orders on exchange. Unfilled orders might be cancelled if timeout
|
||||
@@ -1608,9 +1587,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if not_closed:
|
||||
if fully_cancelled or (
|
||||
open_order
|
||||
and self.strategy.ft_check_timed_out(
|
||||
trade, open_order, datetime.now(timezone.utc)
|
||||
)
|
||||
and self.strategy.ft_check_timed_out(trade, open_order, datetime.now(UTC))
|
||||
):
|
||||
self.handle_cancel_order(
|
||||
order, open_order, trade, constants.CANCEL_REASON["TIMEOUT"]
|
||||
@@ -1708,7 +1685,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade=trade,
|
||||
order=order_obj,
|
||||
pair=trade.pair,
|
||||
current_time=datetime.now(timezone.utc),
|
||||
current_time=datetime.now(UTC),
|
||||
proposed_rate=proposed_rate,
|
||||
current_order_rate=order_obj.safe_placement_price,
|
||||
entry_tag=trade.enter_tag,
|
||||
@@ -2100,7 +2077,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
)(
|
||||
pair=trade.pair,
|
||||
trade=trade,
|
||||
current_time=datetime.now(timezone.utc),
|
||||
current_time=datetime.now(UTC),
|
||||
proposed_rate=proposed_limit_rate,
|
||||
current_profit=current_profit,
|
||||
exit_tag=exit_reason,
|
||||
@@ -2131,7 +2108,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
time_in_force=time_in_force,
|
||||
exit_reason=exit_reason,
|
||||
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}.")
|
||||
@@ -2227,7 +2204,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"enter_tag": trade.enter_tag,
|
||||
"exit_reason": trade.exit_reason,
|
||||
"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_currency": self.config["stake_currency"],
|
||||
"base_currency": self.exchange.get_pair_base_currency(trade.pair),
|
||||
@@ -2272,6 +2249,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"direction": "Short" if trade.is_short else "Long",
|
||||
"gain": gain,
|
||||
"limit": profit_rate or 0,
|
||||
"order_rate": profit_rate or 0,
|
||||
"order_type": order_type,
|
||||
"amount": order.safe_amount_after_fee,
|
||||
"open_rate": trade.open_rate,
|
||||
@@ -2282,7 +2260,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"enter_tag": trade.enter_tag,
|
||||
"exit_reason": trade.exit_reason,
|
||||
"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"],
|
||||
"base_currency": self.exchange.get_pair_base_currency(trade.pair),
|
||||
"quote_currency": self.exchange.get_pair_quote_currency(trade.pair),
|
||||
@@ -2362,18 +2340,19 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
def _update_trade_after_fill(self, trade: Trade, order: Order, send_msg: bool) -> Trade:
|
||||
if order.status in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
strategy_safe_wrapper(self.strategy.order_filled, default_retval=None)(
|
||||
pair=trade.pair, trade=trade, order=order, current_time=datetime.now(timezone.utc)
|
||||
strategy_safe_wrapper(self.strategy.order_filled, supress_error=True)(
|
||||
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 order.ft_order_side == trade.entry_side:
|
||||
if send_msg:
|
||||
if trade.nr_of_successful_entries > 1:
|
||||
# Reset fee_open_currency so fee checking can work
|
||||
# Only necessary for additional entries
|
||||
trade.fee_open_currency = None
|
||||
# Don't cancel stoploss in recovery modes immediately
|
||||
trade = self.cancel_stoploss_on_exchange(trade)
|
||||
if not self.edge:
|
||||
# TODO: should shorting/leverage be supported by Edge,
|
||||
# then this will need to be fixed.
|
||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||
if (
|
||||
order.ft_order_side == trade.entry_side
|
||||
or (trade.amount > 0 and trade.is_open)
|
||||
@@ -2389,14 +2368,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
stake_currency=self.config["stake_currency"],
|
||||
dry_run=self.config["dry_run"],
|
||||
)
|
||||
if self.strategy.use_custom_stoploss:
|
||||
current_rate = self.exchange.get_rate(
|
||||
trade.pair, side="exit", is_short=trade.is_short, refresh=True
|
||||
)
|
||||
profit = trade.calc_profit_ratio(current_rate)
|
||||
self.strategy.ft_stoploss_adjust(
|
||||
current_rate, trade, datetime.now(timezone.utc), profit, 0, after_fill=True
|
||||
)
|
||||
if self.strategy.use_custom_stoploss and trade.is_open:
|
||||
current_rate = self.exchange.get_rate(
|
||||
trade.pair, side="exit", is_short=trade.is_short, refresh=True
|
||||
)
|
||||
profit = trade.calc_profit_ratio(current_rate)
|
||||
self.strategy.ft_stoploss_adjust(
|
||||
current_rate, trade, datetime.now(UTC), profit, 0, after_fill=True
|
||||
)
|
||||
# Updating wallets when order is closed
|
||||
self.wallets.update()
|
||||
return trade
|
||||
@@ -2421,7 +2400,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
def handle_protections(self, pair: str, side: LongShort) -> None:
|
||||
# Lock pair for one candle to prevent immediate re-entries
|
||||
self.strategy.lock_pair(pair, datetime.now(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)
|
||||
if prot_trig:
|
||||
msg: RPCProtectionMsg = {
|
||||
@@ -2476,10 +2455,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
return None
|
||||
|
||||
def handle_order_fee(self, trade: Trade, order_obj: Order, order: CcxtOrder) -> None:
|
||||
# Try update amount (binance-fix)
|
||||
# Try update amount (binance-fix - but also applies to different exchanges)
|
||||
try:
|
||||
fee_abs = self.get_real_amount(trade, order, order_obj)
|
||||
if fee_abs is not None:
|
||||
if (fee_abs := self.get_real_amount(trade, order, order_obj)) is not None:
|
||||
order_obj.ft_fee_base = fee_abs
|
||||
except DependencyException as exception:
|
||||
logger.warning("Could not update trade amount: %s", exception)
|
||||
@@ -2496,9 +2474,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
order_amount = safe_value_fallback(order, "filled", "amount")
|
||||
# Only run for closed orders
|
||||
if (
|
||||
trade.fee_updated(order.get("side", ""))
|
||||
or order["status"] == "open"
|
||||
or order_obj.ft_fee_base
|
||||
trade.fee_updated(order.get("side", "")) or order["status"] == "open"
|
||||
# or order_obj.ft_fee_base
|
||||
):
|
||||
return None
|
||||
|
||||
|
||||
@@ -8,4 +8,4 @@ from freqtrade.ft_types.backtest_result_type import (
|
||||
get_BacktestResultType_default,
|
||||
)
|
||||
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 typing import Literal
|
||||
from typing import Literal, Required
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
from typing_extensions import Required, TypedDict
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class AnnotationType(TypedDict, total=False):
|
||||
|
||||
@@ -58,7 +58,7 @@ def setup_logging_pre() -> None:
|
||||
FT_LOGGING_CONFIG = {
|
||||
"version": 1,
|
||||
# "incremental": True,
|
||||
# "disable_existing_loggers": False,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"basic": {"format": "%(message)s"},
|
||||
"standard": {
|
||||
@@ -223,7 +223,7 @@ def setup_logging(config: Config) -> None:
|
||||
logger.info("Enabling colorized output.")
|
||||
error_console._color_system = error_console._detect_color_system()
|
||||
|
||||
logging.info("Logfile configured")
|
||||
logger.info("Logfile configured")
|
||||
|
||||
# Set verbosity levels
|
||||
logging.root.setLevel(logging.INFO if verbosity < 1 else logging.DEBUG)
|
||||
|
||||
@@ -21,7 +21,9 @@ class FtRichHandler(Handler):
|
||||
msg = self.format(record)
|
||||
# Format log message
|
||||
log_time = Text(
|
||||
datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S,%f")[:-3],
|
||||
datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S,%f")[:-3]
|
||||
if record.created
|
||||
else "N/A",
|
||||
)
|
||||
name = Text(record.name, style="violet")
|
||||
log_level = Text(record.levelname, style=f"logging.level.{record.levelname.lower()}")
|
||||
@@ -40,5 +42,8 @@ class FtRichHandler(Handler):
|
||||
|
||||
except RecursionError:
|
||||
raise
|
||||
except ImportError:
|
||||
# Error when shutting down the console...
|
||||
pass
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@ from typing import Any
|
||||
|
||||
|
||||
# check min. python version
|
||||
if sys.version_info < (3, 10): # pragma: no cover # noqa: UP036
|
||||
sys.exit("Freqtrade requires Python version >= 3.10")
|
||||
if sys.version_info < (3, 11): # pragma: no cover # noqa: UP036
|
||||
sys.exit("Freqtrade requires Python version >= 3.11")
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.commands import Arguments
|
||||
|
||||
@@ -125,6 +125,7 @@ class LookaheadAnalysis(BaseAnalysis):
|
||||
|
||||
backtesting = Backtesting(prepare_data_config, self.exchange)
|
||||
self.exchange = backtesting.exchange
|
||||
self.local_config["candle_type_def"] = prepare_data_config["candle_type_def"]
|
||||
self._fee = backtesting.fee
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
|
||||
|
||||
@@ -743,7 +743,7 @@ class Backtesting:
|
||||
if order and self._get_order_filled(order.ft_price, row):
|
||||
order.close_bt_order(current_date, trade)
|
||||
self._run_funding_fees(trade, current_date, force=True)
|
||||
strategy_safe_wrapper(self.strategy.order_filled, default_retval=None)(
|
||||
strategy_safe_wrapper(self.strategy.order_filled, supress_error=True)(
|
||||
pair=trade.pair,
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
order=order,
|
||||
@@ -1822,7 +1822,11 @@ class Backtesting:
|
||||
# Update old results with new ones.
|
||||
if len(self.all_bt_content) > 0:
|
||||
results = generate_backtest_stats(
|
||||
data, self.all_bt_content, min_date=min_date, max_date=max_date
|
||||
data,
|
||||
self.all_bt_content,
|
||||
min_date=min_date,
|
||||
max_date=max_date,
|
||||
notes=self.config.get("backtest_notes"),
|
||||
)
|
||||
if self.results:
|
||||
self.results["metadata"].update(results["metadata"])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from pandas import DataFrame
|
||||
@@ -38,7 +38,7 @@ class BaseAnalysis:
|
||||
|
||||
@staticmethod
|
||||
def dt_to_timestamp(dt: datetime):
|
||||
timestamp = int(dt.replace(tzinfo=timezone.utc).timestamp())
|
||||
timestamp = int(dt.replace(tzinfo=UTC).timestamp())
|
||||
return timestamp
|
||||
|
||||
def fill_full_varholder(self):
|
||||
@@ -48,12 +48,12 @@ class BaseAnalysis:
|
||||
parsed_timerange = TimeRange.parse_timerange(self.local_config["timerange"])
|
||||
|
||||
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:
|
||||
self.full_varHolder.from_dt = parsed_timerange.startdt
|
||||
|
||||
if parsed_timerange.stopdt is None:
|
||||
self.full_varHolder.to_dt = datetime.now(timezone.utc)
|
||||
self.full_varHolder.to_dt = datetime.now(UTC)
|
||||
else:
|
||||
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