Compare commits
570 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc807d1cf9 | |||
| 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 | |||
| 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 | |||
| 28399aaab2 | |||
| eb5d5e0d3f | |||
| 560085ab92 | |||
| 17c315bf4a | |||
| e8145e0057 | |||
| ae9073885f | |||
| 61886942c5 | |||
| 12d31c4acb | |||
| 14cfdb13c5 | |||
| e58ab7852a | |||
| 77dcc5354c | |||
| 3ac4a872b1 | |||
| b8b94d58c4 | |||
| dfae7ca2ec | |||
| 17cd0452ec | |||
| 33b5482065 | |||
| 9fe361f6fc | |||
| da7f35db8d | |||
| 4f216eb861 | |||
| 53383f3184 | |||
| b51c937e87 | |||
| f3d7f67671 | |||
| 87001d0806 | |||
| ffce0dfb2e | |||
| c1d08019d9 | |||
| bfa95ad976 | |||
| 4a4b39f898 | |||
| 215499a3f8 | |||
| 8c11974644 | |||
| 7dcbd8108e | |||
| 43fea43363 | |||
| e00b74c0e0 | |||
| 5ef439dd0f | |||
| 538c7b3c97 | |||
| 81456b2340 | |||
| dfec3f16ab | |||
| e67499e6cc | |||
| a1b5e7899f | |||
| a88a149999 | |||
| 168593d7af | |||
| c646abc05d | |||
| f2a1561d31 | |||
| d7a3aeaa1b | |||
| 4b9a63de19 | |||
| 07985c2cae | |||
| 0c13414f14 | |||
| 1021c7a7af | |||
| 5306fb3709 | |||
| 1afddf9db4 | |||
| 929538dd5f | |||
| f432e65a14 | |||
| dfc1193c52 | |||
| 25077a1008 | |||
| b3b3bf6c1d | |||
| 29ae71333a | |||
| a7ebbd89c0 | |||
| ee25635c79 | |||
| b8e19ae78a | |||
| 937bd892fd | |||
| adce6e4f68 | |||
| 0b1ba0458d | |||
| 7668b95900 | |||
| 47465f198c | |||
| f3d03dd26a | |||
| ecdd84efd8 | |||
| 0ad6a6a951 | |||
| dc97f709d7 | |||
| 34ccf141d1 | |||
| f0b16c277c | |||
| 68061444d8 | |||
| 93356286d8 | |||
| 84e816fc5b | |||
| 12a482a6ba | |||
| a26a40d00f | |||
| 64c6b4718d | |||
| d1ebdf5df2 | |||
| 496c4947bd | |||
| 82f98746bd | |||
| 65418c7c00 | |||
| bbf6bade7c | |||
| b6cde05f0d | |||
| 0a3a1d7eca | |||
| 947cbdd858 | |||
| 7a84d1ecba | |||
| 8c999cd60c | |||
| ad5d2a36bc | |||
| 1666746692 | |||
| 3a74fc85b4 | |||
| 92a1c304a8 | |||
| d8c24663fc | |||
| 195c15c8dc | |||
| 44c06f6d20 | |||
| c268f20d18 | |||
| 83c92f3ac1 | |||
| b4a3eccfa3 | |||
| 837d006e11 | |||
| c1f1639807 | |||
| 8f71e45666 | |||
| 17fa2c9933 | |||
| 22fdf157ef | |||
| 6c8b39d32a | |||
| 5a2b3d9d87 | |||
| 8641796d04 | |||
| c5c5cfb4d5 | |||
| c74033d7f1 | |||
| d8c6873ee9 | |||
| b0cc2a2a1d | |||
| 226f9b7857 | |||
| 1abf72855e | |||
| b044410e42 | |||
| b64052850f | |||
| 8868b10042 | |||
| b18190891d | |||
| 5b001eec3e | |||
| 882ca44816 | |||
| 60157606fe | |||
| cbb0294cd2 | |||
| 1b4802ab73 | |||
| 6b7f210ef3 | |||
| 571b154345 | |||
| 9a2b5a1219 | |||
| fe9de41d58 | |||
| daff50e79e | |||
| 09b687d66f | |||
| 665ff9f14e | |||
| bd1a12ceaa | |||
| d33e931a0d | |||
| fbe38c362c | |||
| 718efc828a | |||
| 6d3ed84807 | |||
| 28e5efc902 | |||
| 510c47bd78 | |||
| 2e0065216f | |||
| ce024c74e1 | |||
| 90e2935abf | |||
| 12cd654bce | |||
| c3ea793abb | |||
| c901f6d1dd | |||
| 193cfb634c | |||
| a2b7620b64 | |||
| 9d91a4a298 | |||
| 3b6dbaccf8 | |||
| 3fe9e6b82b | |||
| b6614045aa | |||
| cbf27f0bdb | |||
| bc78e2e1b9 | |||
| deafe39f76 | |||
| 1f63eca1a8 | |||
| 3fb6bc888b | |||
| 2c0db908b5 | |||
| ca9403f981 | |||
| a8d96087c1 | |||
| 1649505121 | |||
| 41a063e2ac | |||
| ba019ad143 | |||
| 6f3071f6ce | |||
| 5cdb2dd601 | |||
| 4db41f0225 | |||
| cc8581164d | |||
| 7d50353697 | |||
| d742f8b88f | |||
| 22bf036a66 | |||
| c45351beff | |||
| 6d1d0751e8 | |||
| 15012ec58c | |||
| fed91ff5a1 | |||
| 0228ec796b | |||
| 5b92af6a90 | |||
| 442a1ba50d | |||
| c3f6aa17ee | |||
| 69de7b4045 | |||
| 8dc278f1a0 | |||
| 5f907d4219 | |||
| c39c5f254f | |||
| 0dfc4ed696 | |||
| 950a0df8b1 | |||
| 2a0dd4cf55 | |||
| 575c381e65 | |||
| 87061bcce8 | |||
| a6d3995013 | |||
| d0d40f4fce | |||
| ce7d81325d | |||
| 7cfecab7e2 | |||
| 8f8da51808 | |||
| f94fd7d5fc | |||
| befc41ae54 | |||
| 02092926ab | |||
| a49b4ab93a | |||
| bd7549bc8b | |||
| 47e1d209db | |||
| 5d2f5ec12f | |||
| 2d2dc7f14a | |||
| 43bd2a060c | |||
| b518d66aa2 | |||
| 2ba59cebe3 | |||
| f165c85e69 | |||
| fca454bba3 | |||
| 3dc1adfad4 | |||
| 38754e0598 | |||
| 8fb4446d82 | |||
| 3725ac145b | |||
| 7df0d06005 | |||
| 4e855b638b | |||
| 9783aba09c | |||
| 1936b20309 | |||
| 69de9687d0 | |||
| bdeeca7d43 | |||
| a7ba3cad85 | |||
| ef5fcad8cf | |||
| d8de6c8f7b | |||
| c3239da266 | |||
| 1edf948e49 | |||
| 224c4a717b | |||
| 6202436bfd | |||
| 122144857e | |||
| 92bcf672fb | |||
| 6f4ffa0758 | |||
| 82186c8a7f | |||
| 50596e1a6a | |||
| 9edcbbb9a8 | |||
| a59805d739 | |||
| b96c06ffdc | |||
| 8abe358c1e | |||
| 2142b2aea3 | |||
| 53ba3ced06 | |||
| a0f336c31f | |||
| 081fa2209a | |||
| ca67d3fb2c | |||
| 5a6b43da46 | |||
| 7d3fa41911 | |||
| b8f3f5e8d2 | |||
| 4849d5413f | |||
| 8b8bf6f97d | |||
| 9eea958e17 | |||
| f8a151c2a5 | |||
| 1013c32316 | |||
| d9ed7e1fb2 | |||
| 689da4c479 | |||
| 87d954a322 | |||
| 74f601b352 | |||
| a3359b62d4 | |||
| 064708a354 | |||
| 066a46d7aa | |||
| 333660e628 | |||
| 81f80a6b22 | |||
| dd613ac86c | |||
| 73c28890d7 | |||
| 61dd94ceb0 | |||
| 1fec5a873e | |||
| 9c4abcc927 | |||
| b74e38ef63 | |||
| 86f640627f | |||
| e27568a9d8 | |||
| 149133cc44 | |||
| 1b2d5a357f | |||
| ecc6371733 | |||
| 0c66180cf3 | |||
| 5954c64cc3 | |||
| d0b7a52202 | |||
| c18085d173 | |||
| 67f88ea97a | |||
| 14353afa78 | |||
| a77618cc6b | |||
| cac2c363fa | |||
| ced1ce340a | |||
| 1d99b7d0f9 | |||
| 5c47a75f63 | |||
| 1ef2eeb456 | |||
| c32f8e972a | |||
| 04492e75b2 | |||
| 8d0ca7f5c1 | |||
| 0a4f82bc22 | |||
| 5ee7f8037d | |||
| 12728c0fcc | |||
| 362f0895f4 | |||
| fb64ac942b | |||
| 3fc40f45b3 | |||
| ca5ccc8799 | |||
| 057cc2538e | |||
| ed22789a1a | |||
| 2abf22e37b | |||
| 9b08b51ad8 | |||
| 83cdf76636 | |||
| f545113840 | |||
| f86bc71c43 | |||
| 8af9875d45 | |||
| 8a9b31eccd | |||
| b4a5f66f29 | |||
| e105ea660b | |||
| 1149393789 | |||
| d4655660f4 | |||
| e656063771 | |||
| 825c059c2e | |||
| 8e8f4dbdd3 | |||
| 865c253922 | |||
| cd6e06c86e | |||
| 422f225e8e | |||
| 9921c54882 | |||
| 20fca07d8f | |||
| 8ee40ade45 | |||
| 30ead79e11 | |||
| 5c859d929b | |||
| c89058788e | |||
| 35c3868c56 | |||
| 85689ebc1c | |||
| 4fcc9dd587 | |||
| 05f19d574a | |||
| 1a24559729 | |||
| 7a51c9d540 | |||
| 1d22377cad | |||
| 90aaaa50fc | |||
| bffb3120c1 | |||
| 3fcf6559ab | |||
| 85f4a8daea | |||
| 2595479e43 | |||
| b603771242 | |||
| 9c1183bc59 | |||
| 6b78b1c882 | |||
| ee3d46c8fa | |||
| 59e52bb601 | |||
| 553dbccec7 | |||
| 2e06eb0e7b | |||
| 31c4d35932 | |||
| eb03382b2d | |||
| fcd0c1d606 | |||
| 276ef6cdde | |||
| c5088e6b66 | |||
| 62f05964b4 | |||
| b6a65ce125 | |||
| 315421037f | |||
| 4ff200d315 | |||
| 5700f789e2 | |||
| 689767e3ca | |||
| 66efc9a469 | |||
| 4c3e477667 | |||
| 8cdae5f56e | |||
| 347295ecaf | |||
| 7272204d58 |
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
about: Create a report to help us improve. Do not use this for strategy assistance.
|
||||
title: ''
|
||||
labels: "Triage Needed"
|
||||
assignees: ''
|
||||
@@ -12,6 +12,10 @@ Have you searched for similar issues before posting it?
|
||||
If you have discovered a bug in the bot, please [search the issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
|
||||
If it hasn't been reported, please create a new issue.
|
||||
|
||||
Has your strategy or configuration been generated by an AI model, and is now not working?
|
||||
This is almost certainly NOT a bug in Freqtrade, but a problem with the code your AI model generated.
|
||||
Please consult the documentation. We'll close such issues and point to the documentation.
|
||||
|
||||
Please do not use the bug report template to request new features.
|
||||
-->
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
about: Suggest a new feature or idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask a question you could not find an answer in the docs
|
||||
about: Ask a question you could not find an answer in the docs. Use this template if you've got problems with your strategy.
|
||||
title: ''
|
||||
labels: "Question"
|
||||
assignees: ''
|
||||
|
||||
+25
-107
@@ -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.10", "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@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
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.10", "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@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
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.10", "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@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
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
|
||||
@@ -434,7 +437,7 @@ jobs:
|
||||
|
||||
build-linux-online:
|
||||
# Run pytest with "live" checks
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -446,7 +449,7 @@ jobs:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
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,132 @@
|
||||
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
|
||||
|
||||
# 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@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 }}
|
||||
@@ -14,24 +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.16.1"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==5.5.0.20240820
|
||||
- types-cachetools==6.0.0.20250525
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.32.0.20250328
|
||||
- types-requests==2.32.4.20250611
|
||||
- types-tabulate==0.9.0.20241207
|
||||
- types-python-dateutil==2.9.0.20241206
|
||||
- SQLAlchemy==2.0.40
|
||||
- types-python-dateutil==2.9.0.20250516
|
||||
- SQLAlchemy==2.0.41
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
@@ -43,7 +43,7 @@ repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.11.7'
|
||||
rev: 'v0.12.1'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
@@ -69,7 +69,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/stefmolin/exif-stripper
|
||||
rev: 0.6.2
|
||||
rev: 1.0.0
|
||||
hooks:
|
||||
- id: strip-exif
|
||||
|
||||
@@ -82,6 +82,6 @@ repos:
|
||||
|
||||
# Ensure github actions remain safe
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.6.0
|
||||
rev: v1.11.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
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
|
||||
@@ -35,7 +35,7 @@ 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
|
||||
|
||||
@@ -70,7 +70,6 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
||||
- [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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
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 -U wheel "numpy<3.0"
|
||||
pip install --only-binary ta-lib --find-links=build_helpers\ ta-lib
|
||||
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
+31
-62
@@ -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"
|
||||
@@ -610,11 +606,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"chat_id": {
|
||||
"description": "Telegram chat or group ID",
|
||||
"description": "Telegram chat or group ID. Recommended to be set via environment variable FREQTRADE__TELEGRAM__CHAT_ID",
|
||||
"type": "string"
|
||||
},
|
||||
"topic_id": {
|
||||
"description": "Telegram topic ID - only applicable for group chats",
|
||||
"description": "Telegram topic ID - only applicable for group chats. Recommended to be set via environment variable FREQTRADE__TELEGRAM__TOPIC_ID",
|
||||
"type": "string"
|
||||
},
|
||||
"authorized_users": {
|
||||
@@ -773,9 +769,11 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Enable webhook notifications.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"url": {
|
||||
"description": "Webhook URL. Recommended to be set via environment variable FREQTRADE__WEBHOOK__URL",
|
||||
"type": "string"
|
||||
},
|
||||
"format": {
|
||||
@@ -853,6 +851,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"webhook_url": {
|
||||
"description": "Discord webhook URL. Recommended to be set via environment variable FREQTRADE__DISCORD__WEBHOOK_URL",
|
||||
"type": "string"
|
||||
},
|
||||
"exit_fill": {
|
||||
@@ -1168,28 +1167,35 @@
|
||||
"description": "Name of the exchange.",
|
||||
"type": "string"
|
||||
},
|
||||
"enable_ws": {
|
||||
"description": "Enable WebSocket connections to the exchange.",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"key": {
|
||||
"description": "API key for the exchange.",
|
||||
"description": "API key for the exchange. Recommended to be set via environment variable FREQTRADE__EXCHANGE__KEY",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"secret": {
|
||||
"description": "API secret for the exchange.",
|
||||
"description": "API secret for the exchange. Recommended to be set via environment variable FREQTRADE__EXCHANGE__SECRET",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"password": {
|
||||
"description": "Password for the exchange, if required.",
|
||||
"description": "Password for the exchange, if required. Recommended to be set via environment variable FREQTRADE__EXCHANGE__PASSWORD",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"uid": {
|
||||
"description": "User ID for the exchange, if required.",
|
||||
"description": "User ID for the exchange, if required. Recommended to be set via environment variable FREQTRADE__EXCHANGE__UID",
|
||||
"type": "string"
|
||||
},
|
||||
"account_id": {
|
||||
"description": "Account ID for the exchange, if required. Recommended to be set via environment variable FREQTRADE__EXCHANGE__ACCOUNT_ID",
|
||||
"type": "string"
|
||||
},
|
||||
"wallet_address": {
|
||||
"description": "Wallet address for the exchange, if required. Usually used by DEX exchanges. Recommended to be set via environment variable FREQTRADE__EXCHANGE__WALLET_ADDRESS",
|
||||
"type": "string"
|
||||
},
|
||||
"private_key": {
|
||||
"description": "Private key for the exchange, if required. Usually used by DEX exchanges. Recommended to be set via environment variable FREQTRADE__EXCHANGE__PRIVATE_KEY",
|
||||
"type": "string"
|
||||
},
|
||||
"pair_whitelist": {
|
||||
@@ -1213,6 +1219,11 @@
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"enable_ws": {
|
||||
"description": "Enable WebSocket connections to the exchange.",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"unknown_fee_rate": {
|
||||
"description": "Fee rate for unknown markets.",
|
||||
"type": "number"
|
||||
@@ -1232,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"
|
||||
}
|
||||
},
|
||||
@@ -1240,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,4 +1,4 @@
|
||||
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
|
||||
@@ -34,7 +34,7 @@ COPY build_helpers/* /tmp/
|
||||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
||||
USER ftuser
|
||||
RUN pip install --user --no-cache-dir "numpy<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
|
||||
|
||||
|
||||
+35
-38
@@ -161,56 +161,53 @@ class MyAwesomeStrategy(IStrategy):
|
||||
|
||||
### Overriding Base estimator
|
||||
|
||||
You can define your own estimator for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass.
|
||||
You can define your own optuna sampler for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass.
|
||||
|
||||
```python
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
class HyperOpt:
|
||||
def generate_estimator(dimensions: List['Dimension'], **kwargs):
|
||||
return "RF"
|
||||
return "NSGAIIISampler"
|
||||
|
||||
```
|
||||
|
||||
Possible values are either one of "GP", "RF", "ET", "GBRT" (Details can be found in the [scikit-optimize documentation](https://scikit-optimize.github.io/)), or "an instance of a class that inherits from `RegressorMixin` (from sklearn) and where the `predict` method has an optional `return_std` argument, which returns `std(Y | x)` along with `E[Y | x]`".
|
||||
Possible values are either one of "NSGAIISampler", "TPESampler", "GPSampler", "CmaEsSampler", "NSGAIIISampler", "QMCSampler" (Details can be found in the [optuna-samplers documentation](https://optuna.readthedocs.io/en/stable/reference/samplers/index.html)), or "an instance of a class that inherits from `optuna.samplers.BaseSampler`".
|
||||
|
||||
Some research will be necessary to find additional Regressors.
|
||||
|
||||
Example for `ExtraTreesRegressor` ("ET") with additional parameters:
|
||||
|
||||
```python
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
class HyperOpt:
|
||||
def generate_estimator(dimensions: List['Dimension'], **kwargs):
|
||||
from skopt.learning import ExtraTreesRegressor
|
||||
# Corresponds to "ET" - but allows additional parameters.
|
||||
return ExtraTreesRegressor(n_estimators=100)
|
||||
|
||||
```
|
||||
|
||||
The `dimensions` parameter is the list of `skopt.space.Dimension` objects corresponding to the parameters to be optimized. It can be used to create isotropic kernels for the `skopt.learning.GaussianProcessRegressor` estimator. Here's an example:
|
||||
|
||||
```python
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
class HyperOpt:
|
||||
def generate_estimator(dimensions: List['Dimension'], **kwargs):
|
||||
from skopt.utils import cook_estimator
|
||||
from skopt.learning.gaussian_process.kernels import (Matern, ConstantKernel)
|
||||
kernel_bounds = (0.0001, 10000)
|
||||
kernel = (
|
||||
ConstantKernel(1.0, kernel_bounds) *
|
||||
Matern(length_scale=np.ones(len(dimensions)), length_scale_bounds=[kernel_bounds for d in dimensions], nu=2.5)
|
||||
)
|
||||
kernel += (
|
||||
ConstantKernel(1.0, kernel_bounds) *
|
||||
Matern(length_scale=np.ones(len(dimensions)), length_scale_bounds=[kernel_bounds for d in dimensions], nu=1.5)
|
||||
)
|
||||
|
||||
return cook_estimator("GP", space=dimensions, kernel=kernel, n_restarts_optimizer=2)
|
||||
```
|
||||
Some research will be necessary to find additional Samplers (from optunahub) for example.
|
||||
|
||||
!!! Note
|
||||
While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used.
|
||||
If you're unsure about this, best use one of the Defaults (`"ET"` has proven to be the most versatile) without further parameters.
|
||||
If you're unsure about this, best use one of the Defaults (`"NSGAIIISampler"` has proven to be the most versatile) without further parameters.
|
||||
|
||||
??? Example "Using `AutoSampler` from Optunahub"
|
||||
|
||||
[AutoSampler docs](https://hub.optuna.org/samplers/auto_sampler/)
|
||||
|
||||
Install the necessary dependencies
|
||||
``` bash
|
||||
pip install optunahub cmaes torch scipy
|
||||
```
|
||||
Implement `generate_estimator()` in your strategy
|
||||
|
||||
``` python
|
||||
# ...
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from typing import List
|
||||
import optunahub
|
||||
# ...
|
||||
|
||||
class my_strategy(IStrategy):
|
||||
class HyperOpt:
|
||||
def generate_estimator(dimensions: List["Dimension"], **kwargs):
|
||||
if "random_state" in kwargs.keys():
|
||||
return optunahub.load_module("samplers/auto_sampler").AutoSampler(seed=kwargs["random_state"])
|
||||
else:
|
||||
return optunahub.load_module("samplers/auto_sampler").AutoSampler()
|
||||
|
||||
```
|
||||
|
||||
Obviously the same approach will work for all other Samplers optuna supports.
|
||||
|
||||
|
||||
## Space options
|
||||
|
||||
|
||||
+8
-11
@@ -315,7 +315,6 @@ $RepeatedMsgReduction on
|
||||
|
||||
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
|
||||
|
||||
|
||||
So, the following are the examples of possible addresses:
|
||||
|
||||
* `"address": "/dev/log"` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
|
||||
@@ -323,20 +322,18 @@ So, the following are the examples of possible addresses:
|
||||
* `"address": "localhost:514"` -- log to local syslog using UDP socket, if it listens on port 514.
|
||||
* `"address": "<ip>:514"` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
|
||||
|
||||
|
||||
??? Info "Deprecated - configure syslog via command line"
|
||||
`--logfile syslog:<syslog_address>` -- send log messages to `syslog` service using the `<syslog_address>` as the syslog address.
|
||||
|
||||
`--logfile syslog:<syslog_address>` -- send log messages to `syslog` service using the `<syslog_address>` as the syslog address.
|
||||
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
|
||||
|
||||
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
|
||||
So, the following are the examples of possible usages:
|
||||
|
||||
So, the following are the examples of possible usages:
|
||||
|
||||
* `--logfile syslog:/dev/log` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
|
||||
* `--logfile syslog` -- same as above, the shortcut for `/dev/log`.
|
||||
* `--logfile syslog:/var/run/syslog` -- log to syslog (rsyslog) using the `/var/run/syslog` socket. Use this on MacOS.
|
||||
* `--logfile syslog:localhost:514` -- log to local syslog using UDP socket, if it listens on port 514.
|
||||
* `--logfile syslog:<ip>:514` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
|
||||
* `--logfile syslog:/dev/log` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
|
||||
* `--logfile syslog` -- same as above, the shortcut for `/dev/log`.
|
||||
* `--logfile syslog:/var/run/syslog` -- log to syslog (rsyslog) using the `/var/run/syslog` socket. Use this on MacOS.
|
||||
* `--logfile syslog:localhost:514` -- log to local syslog using UDP socket, if it listens on port 514.
|
||||
* `--logfile syslog:<ip>:514` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
|
||||
|
||||
### Logging to journald
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
@@ -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"
|
||||
@@ -435,6 +437,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).
|
||||
|
||||
@@ -16,6 +16,7 @@ usage: freqtrade hyperopt [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[--random-state INT] [--min-trades INT]
|
||||
[--hyperopt-loss NAME] [--disable-param-export]
|
||||
[--ignore-missing-spaces] [--analyze-per-epoch]
|
||||
[--early-stop INT]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -87,6 +88,8 @@ options:
|
||||
Suppress errors for any requested Hyperopt spaces that
|
||||
do not contain any parameters.
|
||||
--analyze-per-epoch Run populate_indicators once per epoch.
|
||||
--early-stop INT Early stop hyperopt if no improvement after (default:
|
||||
0) epochs.
|
||||
|
||||
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
|
||||
|
||||
@@ -180,7 +180,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to exit a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
||||
| `stoploss` | **Required.** Value as ratio of the stoploss used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float (as ratio)
|
||||
| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md#trailing-stop-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Boolean
|
||||
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
|
||||
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-different-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
|
||||
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
|
||||
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
|
||||
@@ -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
|
||||
@@ -672,7 +671,7 @@ Should you experience problems you suspect are caused by websockets, you can dis
|
||||
}
|
||||
```
|
||||
|
||||
Should you be required to use a proxy, please refer to the [proxy section](#using-proxy-with-freqtrade) for more information.
|
||||
Should you be required to use a proxy, please refer to the [proxy section](#using-a-proxy-with-freqtrade) for more information.
|
||||
|
||||
!!! Info "Rollout"
|
||||
We're implementing this out slowly, ensuring stability of your bots.
|
||||
|
||||
@@ -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/
|
||||
@@ -367,6 +367,25 @@ Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer
|
||||
|
||||
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.
|
||||
|
||||
## Bitvavo
|
||||
|
||||
If your account is required to use an operatorId, you can set it in the configuration file as follows:
|
||||
|
||||
``` json
|
||||
"exchange": {
|
||||
"name": "bitvavo",
|
||||
"key": "",
|
||||
"secret": "",
|
||||
"ccxt_config": {
|
||||
"options": {
|
||||
"operatorId": "123567"
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Bitvavo expects the `operatorId` to be an integer.
|
||||
|
||||
## All exchanges
|
||||
|
||||
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
||||
|
||||
+9
-18
@@ -102,6 +102,14 @@ You can use "current" market data by using the [dataprovider](strategy-customiza
|
||||
|
||||
You can use the `/stopentry` command in Telegram to prevent future trade entry, followed by `/forceexit all` (sell all open trades).
|
||||
|
||||
### I sold the bot's capital and now there's errors in the log
|
||||
|
||||
Freqtrade assumes that the trades it opens are managed only though the bot.
|
||||
If you happen to (accidentally) sell the bot's capital, freqtrade will try to recover by trying to re-find on-exchange orders.
|
||||
|
||||
This is a best-effort approach, and will not work in all cases, especially when using order types that are not supported by freqtrade (OCO, iceberg, etc.), or when working with older trades (where the exchange no longer provides full order information).
|
||||
The exact limits will vary between exchanges - with the details usually being documented in the exchange's API documentation.
|
||||
|
||||
### I want to run multiple bots on the same machine
|
||||
|
||||
Please look at the [advanced setup documentation Page](advanced-setup.md#running-multiple-instances-of-freqtrade).
|
||||
@@ -219,10 +227,7 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us
|
||||
First of all, most indicator libraries don't have GPU support - as such, there would be little benefit for indicator calculations.
|
||||
The GPU improvements would only apply to pandas-native calculations - or ones written by yourself.
|
||||
|
||||
For hyperopt, freqtrade is using scikit-optimize, which is built on top of scikit-learn.
|
||||
Their statement about GPU support is [pretty clear](https://scikit-learn.org/stable/faq.html#will-you-add-gpu-support).
|
||||
|
||||
GPU's also are only good at crunching numbers (floating point operations).
|
||||
GPU's are only good at crunching numbers (floating point operations).
|
||||
For hyperopt, we need both number-crunching (find next parameters) and running python code (running backtesting).
|
||||
As such, GPU's are not too well suited for most parts of hyperopt.
|
||||
|
||||
@@ -271,20 +276,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:
|
||||
|
||||
@@ -181,7 +181,7 @@ You can ask for each of the defined features to be included also for informative
|
||||
In total, the number of features the user of the presented example strategy has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
|
||||
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
||||
|
||||
!!! note "Learn more about creative feature engineering"
|
||||
!!! note "Learn more about creative feature engineering"
|
||||
Check out our [medium article](https://emergentmethods.medium.com/freqai-from-price-to-prediction-6fadac18b665) geared toward helping users learn how to creatively engineer features.
|
||||
|
||||
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
||||
@@ -310,7 +310,7 @@ class MyCoolTransform(BaseTransform):
|
||||
|
||||
If you have created your own custom `IFreqaiModel` with a custom `train()`/`predict()` function, *and* you still rely on `data_cleaning_train/predict()`, then you will need to migrate to the new pipeline. If your model does *not* rely on `data_cleaning_train/predict()`, then you do not need to worry about this migration.
|
||||
|
||||
More details about the migration can be found [here](strategy_migration.md#freqai---new-data-pipeline).
|
||||
More details about the migration can be found [here](strategy_migration.md#freqai-new-data-pipeline).
|
||||
|
||||
## Outlier detection
|
||||
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ pip install -r requirements-freqai.txt
|
||||
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:stable_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices. If you would like to use PyTorch or Reinforcement learning, you should use the torch or RL tags, `image: freqtradeorg/freqtrade:stable_freqaitorch`, `image: freqtradeorg/freqtrade:stable_freqairl`.
|
||||
|
||||
!!! note "docker-compose-freqai.yml"
|
||||
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
|
||||
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
|
||||
|
||||
### FreqAI position in open-source machine learning landscape
|
||||
|
||||
|
||||
+5
-3
@@ -1,10 +1,10 @@
|
||||
# Hyperopt
|
||||
|
||||
This page explains how to tune your strategy by finding the optimal
|
||||
parameters, a process called hyperparameter optimization. The bot uses algorithms included in the `scikit-optimize` package to accomplish this.
|
||||
parameters, a process called hyperparameter optimization. The bot uses algorithms included in the `optuna` package to accomplish this.
|
||||
The search will burn all your CPU cores, make your laptop sound like a fighter jet and still take a long time.
|
||||
|
||||
In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses Bayesian search with a ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions).
|
||||
In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses one of optuna's sampler algorithms (currently NSGAIIISampler) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions).
|
||||
|
||||
Hyperopt requires historic data to be available, just as backtesting does (hyperopt runs backtesting many times with different parameters).
|
||||
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.
|
||||
@@ -490,6 +490,8 @@ freqtrade hyperopt --config config.json --hyperopt-loss <hyperoptlossname> --str
|
||||
```
|
||||
|
||||
The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs.
|
||||
The `--early-stop` option will set after how many epochs with no improvements hyperopt will stop. A good value is 20-30% of the total epochs. Any value greater than 0 and lower than 20 it will be replaced by 20. Early stop is by default disabled (`--early-stop=0`)
|
||||
|
||||
Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results.
|
||||
|
||||
The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below.
|
||||
@@ -532,7 +534,7 @@ Legal values are:
|
||||
* `trailing`: search for the best trailing stop values
|
||||
* `trades`: search for the best max open trades values
|
||||
* `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these)
|
||||
* `default`: `all` except `trailing` and `protection`
|
||||
* `default`: `all` except `trailing`, `trades` and `protection`
|
||||
* space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||
|
||||
The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
## Exit logic comparisons
|
||||
|
||||
Freqtrade allows your strategy to implement different exit logic using signal-based or callback-based functions.
|
||||
This section aims to compare each different function, helping you to choose the one that best fits your needs.
|
||||
|
||||
* **`populate_exit_trend()`** - Vectorized signal-based exit logic using indicators in the main dataframe
|
||||
✅ **Use** to define exit signals based on indicators or other data that can be calculated in a vectorized manner.
|
||||
🚫 **Don't use** to customize exit conditions for each individual trade, or if trade data is necessary to make an exit decision.
|
||||
* **`custom_exit()`** - Custom exit logic that will fully exit a trade immediately, called for every open trade at every bot loop iteration until a trade is closed.
|
||||
✅ **Use** to specify exit conditions for each individual trade (including any additional adjusted orders using `adjust_trade_position()`), or if trade data is necessary to make an exit decision, e.g. using profit data to exit.
|
||||
🚫 **Don't use** when you want to exit using vectorised indicator-based data (use a `populate_exit_trend()` signal instead), or as a proxy for `custom_stoploss()`, and be aware that rate-based exits in backtesting can be inaccurate.
|
||||
* **`custom_stoploss()`** - Custom trailing stoploss, called for every open trade every iteration until a trade is closed. The value returned here is also used for [stoploss on exchange](stoploss.md#stop-loss-on-exchangefreqtrade).
|
||||
✅ **Use** to customize the stoploss logic to set a dynamic stoploss based on trade data or other conditions.
|
||||
🚫 **Don't use** to exit a trade immediately based on a specific condition. Use `custom_exit()` for that purpose.
|
||||
* **`custom_roi()`** - Custom ROI, called for every open trade every iteration until a trade is closed.
|
||||
✅ **Use** to specify a minimum ROI threshold ("take-profit") to exit a trade at this ROI level at some point within the trade duration, based on profit or other conditions.
|
||||
🚫 **Don't use** to exit a trade immediately based on a specific condition. Use `custom_exit()`.
|
||||
🚫 **Don't use** for static ROI. Use `minimal_roi`.
|
||||
+4
-5
@@ -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).
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ OS Specific steps are listed first, the common section below is necessary for al
|
||||
|
||||
|
||||
```bash
|
||||
sudo apt-get install python3-venv libatlas-base-dev cmake curl
|
||||
sudo apt-get install python3-venv libatlas-base-dev cmake curl libffi-dev
|
||||
# Use piwheels.org to speed up installation
|
||||
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf
|
||||
|
||||
|
||||
+70
-37
@@ -1,23 +1,21 @@
|
||||
# Lookahead analysis
|
||||
|
||||
This page explains how to validate your strategy in terms of look ahead bias.
|
||||
This page explains how to validate your strategy in terms of lookahead bias.
|
||||
|
||||
Checking look ahead bias is the bane of any strategy since it is sometimes very easy to introduce backtest bias -
|
||||
but very hard to detect.
|
||||
Lookahead bias is the bane of any strategy since it is sometimes very easy to introduce this bias, but can be very hard to detect.
|
||||
|
||||
Backtesting initializes all timestamps at once and calculates all indicators in the beginning.
|
||||
This means that if your indicators or entry/exit signals could look into future candles and falsify your backtest.
|
||||
Backtesting initializes all timestamps (loads the whole dataframe into memory) and calculates all indicators at once.
|
||||
This means that if your indicators or entry/exit signals look into future candles, this will falsify your backtest.
|
||||
|
||||
Lookahead-analysis requires historic data to be available.
|
||||
The `lookahead-analysis` command 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.
|
||||
`lookahead-analysis` also supports freqai strategies.
|
||||
|
||||
This command is built upon backtesting since it internally chains backtests and pokes at the strategy to provoke it to show look ahead bias.
|
||||
This is done by not looking at the strategy itself - but at the results it returned.
|
||||
The results are things like changed indicator-values and moved entries/exits compared to the full backtest.
|
||||
This command internally chains backtests and pokes at the strategy to provoke it to show lookahead bias.
|
||||
This is done by not looking at the strategy code itself, but at changed indicator values and moved entries/exits compared to the full backtest.
|
||||
|
||||
You can use commands of [Backtesting](backtesting.md).
|
||||
It also supports the lookahead-analysis of freqai strategies.
|
||||
`lookahead-analysis` can use the typical options of [Backtesting](backtesting.md), but forces the following options:
|
||||
|
||||
- `--cache` is forced to "none".
|
||||
- `--max-open-trades` is forced to be at least equal to the number of pairs.
|
||||
@@ -25,48 +23,83 @@ It also supports the lookahead-analysis of freqai strategies.
|
||||
- `--stake-amount` is forced to be a static 10000 (10k).
|
||||
- `--enable-protections` is forced to be off.
|
||||
|
||||
Those are set to avoid users accidentally generating false positives.
|
||||
These are set to avoid users accidentally generating false positives.
|
||||
|
||||
## Lookahead-analysis command reference
|
||||
|
||||
--8<-- "commands/lookahead-analysis.md"
|
||||
|
||||
!!! Note ""
|
||||
The above Output was reduced to options `lookahead-analysis` adds on top of regular backtesting commands.
|
||||
|
||||
### Summary
|
||||
|
||||
Checks a given strategy for look ahead bias via lookahead-analysis
|
||||
Look ahead bias means that the backtest uses data from future candles thereby not making it viable beyond backtesting
|
||||
and producing false hopes for the one backtesting.
|
||||
!!! Note
|
||||
The above output was reduced to options that `lookahead-analysis` adds on top of regular backtesting commands.
|
||||
|
||||
### Introduction
|
||||
|
||||
Many strategies - without the programmer knowing - have fallen prey to look ahead bias.
|
||||
Many strategies, without the programmer knowing, have fallen prey to lookahead bias.
|
||||
This typically makes the strategy backtest look profitable, sometimes to extremes, but this is not realistic as the strategy is "cheating" by looking at data it would not have in dry or live modes.
|
||||
|
||||
Any backtest will populate the full dataframe including all time stamps at the beginning.
|
||||
If the programmer is not careful or oblivious how things work internally
|
||||
(which sometimes can be really hard to find out) then it will just look into the future making the strategy amazing
|
||||
but not realistic.
|
||||
The reason why strategies can "cheat" is because the freqtrade backtesting process populates the full dataframe including all candle timestamps at the outset.
|
||||
If the programmer is not careful or oblivious how things work internally
|
||||
(which sometimes can be really hard to find out) then the strategy will look into the future.
|
||||
|
||||
This command is made to try to verify the validity in the form of the aforementioned look ahead bias.
|
||||
This command is made to try to verify the validity in the form of the aforementioned lookahead bias.
|
||||
|
||||
### How does the command work?
|
||||
|
||||
It will start with a backtest of all pairs to generate a baseline for indicators and entries/exits.
|
||||
After the backtest ran, it will look if the `minimum-trade-amount` is met
|
||||
and if not cancel the lookahead-analysis for this strategy.
|
||||
After this initial backtest runs, it will look if the `minimum-trade-amount` is met and if not cancel the lookahead-analysis for this strategy.
|
||||
If this happens, use a wider timerange to get more trades for the analysis, or use a timerange where more trades occur.
|
||||
|
||||
After setting the baseline it will then do additional runs for every entry and exit separately.
|
||||
When a verification-backtest is done, it will compare the indicators as the signal (either entry or exit) and report the bias.
|
||||
After all signals have been verified or falsified a result-table will be generated for the user to see.
|
||||
After setting the baseline it will then do additional backtest runs for every entry and exit separately.
|
||||
When these verification backtests complete, it will compare the indicators at the signal candles (both entry or exit)
|
||||
and report the bias.
|
||||
After all signals have been verified or falsified a result table will be generated for the user to see.
|
||||
|
||||
### How to find and remove bias? How can I salvage a biased strategy?
|
||||
|
||||
If you found a biased strategy online and want to have the same results, just without bias,
|
||||
then you will be out of luck most of the time.
|
||||
Usually the bias in the strategy is THE driving factor for "too good to be true" profits.
|
||||
Removing conditions or indicators that push the profits up from bias will usually make the strategy significantly worse.
|
||||
You might be able to salvage it partially if the biased indicators or conditions are not the core of the strategy, or there
|
||||
are other entry and exit signals that are not biased.
|
||||
|
||||
### Examples of lookahead-bias
|
||||
|
||||
- `shift(-10)` looks 10 candles into the future.
|
||||
- Using `iloc[]` in populate_* functions to access a specific row in the dataframe.
|
||||
- For-loops are prone to introduce lookahead bias if you don't tightly control which numbers are looped through.
|
||||
- Aggregation functions like `.mean()`, `.min()` and `.max()`, without a rolling window,
|
||||
will calculate the value over the **whole** dataframe, so the signal candle will "see" a value including future candles.
|
||||
A non-biased example would be to look back candles using `rolling()` instead:
|
||||
e.g. `dataframe['volume_mean_12'] = dataframe['volume'].rolling(12).mean()`
|
||||
- `ta.MACD(dataframe, 12, 26, 1)` will introduce bias with a signalperiod of 1.
|
||||
|
||||
### What do the columns in the results table mean?
|
||||
|
||||
- `filename`: name of the checked strategy file
|
||||
- `strategy`: checked strategy class name
|
||||
- `has_bias`: result of the lookahead-analysis. `No` would be good, `Yes` would be bad.
|
||||
- `total_signals`: number of checked signals (default is 20)
|
||||
- `biased_entry_signals`: found bias in that many entries
|
||||
- `biased_exit_signals`: found bias in that many exits
|
||||
- `biased_indicators`: shows you the indicators themselves that are defined in populate_indicators
|
||||
|
||||
You might get false positives in the `biased_exit_signals` if you have biased entry signals paired with those exits.
|
||||
However, a biased entry will usually result in a biased exit too,
|
||||
even if the exit itself does not produce the bias -
|
||||
especially if your entry and exit conditions use the same biased indicator.
|
||||
|
||||
**Address the bias in the entries first, then address the exits.**
|
||||
|
||||
### Caveats
|
||||
|
||||
- `lookahead-analysis` can only verify / falsify the trades it calculated and verified.
|
||||
If the strategy has many different signals / signal types, it's up to you to select appropriate parameters to ensure that all signals have triggered at least once. Not triggered signals will not have been verified.
|
||||
This could lead to a false-negative (the strategy will then be reported as non-biased).
|
||||
- `lookahead-analysis` has access to everything that backtesting has too.
|
||||
Please don't provoke any configs like enabling position stacking.
|
||||
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` amount and neither leftover money in your wallet.
|
||||
- In the results table, the `biased_indicators` column will falsely flag FreqAI target indicators defined in `set_freqai_targets()` as biased. These are not biased and can safely be ignored.
|
||||
If the strategy has many different signals / signal types, it's up to you to select appropriate parameters to ensure that all signals have triggered at least once. Signals that are not triggered will not have been verified.
|
||||
This would lead to a false-negative, i.e. the strategy will be reported as non-biased.
|
||||
- `lookahead-analysis` has access to the same backtesting options and this can introduce problems.
|
||||
Please don't use any options like enabling position stacking as this will distort the number of checked signals.
|
||||
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` slots,
|
||||
and that you have enough capital in the backtest wallet configuration.
|
||||
- In the results table, the `biased_indicators` column
|
||||
will falsely flag FreqAI target indicators defined in `set_freqai_targets()` as biased.
|
||||
**These are not biased and can safely be ignored.**
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect
|
||||
| `ping_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
||||
| `sleep_time` | Sleep time before retrying to connect.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
||||
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `false`.*<br> **Datatype:** Boolean.
|
||||
| `initial_candle_limit` | Initial candles to expect from the Producer.<br>*Defaults to `1500`.*<br> **Datatype:** Integer - Number of candles.
|
||||
| `message_size_limit` | Size limit per message<br>*Defaults to `8`.*<br> **Datatype:** Integer - Megabytes.
|
||||
|
||||
Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
markdown==3.8
|
||||
markdown==3.8.2
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.6.12
|
||||
mkdocs-material==9.6.14
|
||||
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.
|
||||
|
||||
+261
-10
@@ -12,6 +12,7 @@ Currently available callbacks:
|
||||
* [`custom_stake_amount()`](#stake-size-management)
|
||||
* [`custom_exit()`](#custom-exit-signal)
|
||||
* [`custom_stoploss()`](#custom-stoploss)
|
||||
* [`custom_roi()`](#custom-roi)
|
||||
* [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules)
|
||||
* [`check_entry_timeout()` and `check_exit_timeout()`](#custom-order-timeout-rules)
|
||||
* [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation)
|
||||
@@ -26,6 +27,9 @@ Currently available callbacks:
|
||||
|
||||
--8<-- "includes/strategy-imports.md"
|
||||
|
||||
--8<-- "includes/strategy-exit-comparisons.md"
|
||||
|
||||
|
||||
## Bot start
|
||||
|
||||
A simple callback which is called once when the strategy is loaded.
|
||||
@@ -121,7 +125,7 @@ Freqtrade will fall back to the `proposed_stake` value should your code raise an
|
||||
|
||||
Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed.
|
||||
|
||||
Allows to define custom exit signals, indicating that specified position should be sold. This is very useful when we need to customize exit conditions for each individual trade, or if you need trade data to make an exit decision.
|
||||
Allows to define custom exit signals, indicating that specified position should be closed (full exit). This is very useful when we need to customize exit conditions for each individual trade, or if you need trade data to make an exit decision.
|
||||
|
||||
For example you could implement a 1:2 risk-reward ROI with `custom_exit()`.
|
||||
|
||||
@@ -178,6 +182,8 @@ Returning `None` will be interpreted as "no desire to change", and is the only s
|
||||
|
||||
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchangefreqtrade)).
|
||||
|
||||
If you're on futures markets, please take note of the [stoploss and leverage](stoploss.md#stoploss-and-leverage) section, as the stoploss value returned from `custom_stoploss` is the risk for this trade - not the relative price movement.
|
||||
|
||||
!!! Note "Use of dates"
|
||||
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
|
||||
|
||||
@@ -233,7 +239,7 @@ class AwesomeStrategy(IStrategy):
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New stoploss value, relative to the current_rate
|
||||
"""
|
||||
return -0.04
|
||||
return -0.04 * trade.leverage
|
||||
```
|
||||
|
||||
#### Time based trailing stop
|
||||
@@ -255,9 +261,9 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
||||
if current_time - timedelta(minutes=120) > trade.open_date_utc:
|
||||
return -0.05
|
||||
return -0.05 * trade.leverage
|
||||
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
|
||||
return -0.10
|
||||
return -0.10 * trade.leverage
|
||||
return None
|
||||
```
|
||||
|
||||
@@ -284,9 +290,9 @@ class AwesomeStrategy(IStrategy):
|
||||
return stoploss_from_open(0.10, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
||||
if current_time - timedelta(minutes=120) > trade.open_date_utc:
|
||||
return -0.05
|
||||
return -0.05 * trade.leverage
|
||||
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
|
||||
return -0.10
|
||||
return -0.10 * trade.leverage
|
||||
return None
|
||||
```
|
||||
|
||||
@@ -309,10 +315,10 @@ class AwesomeStrategy(IStrategy):
|
||||
**kwargs) -> float | None:
|
||||
|
||||
if pair in ("ETH/BTC", "XRP/BTC"):
|
||||
return -0.10
|
||||
return -0.10 * trade.leverage
|
||||
elif pair in ("LTC/BTC"):
|
||||
return -0.05
|
||||
return -0.15
|
||||
return -0.05 * trade.leverage
|
||||
return -0.15 * trade.leverage
|
||||
```
|
||||
|
||||
#### Trailing stoploss with positive offset
|
||||
@@ -341,7 +347,7 @@ class AwesomeStrategy(IStrategy):
|
||||
desired_stoploss = current_profit / 2
|
||||
|
||||
# Use a minimum of 2.5% and a maximum of 5%
|
||||
return max(min(desired_stoploss, 0.05), 0.025)
|
||||
return max(min(desired_stoploss, 0.05), 0.025) * trade.leverage
|
||||
```
|
||||
|
||||
#### Stepped stoploss
|
||||
@@ -497,6 +503,135 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
|
||||
|
||||
---
|
||||
|
||||
## Custom ROI
|
||||
|
||||
Called for open trade every iteration (roughly every 5 seconds) until a trade is closed.
|
||||
|
||||
The usage of the custom ROI method must be enabled by setting `use_custom_roi=True` on the strategy object.
|
||||
|
||||
This method allows you to define a custom minimum ROI threshold for exiting a trade, expressed as a ratio (e.g., `0.05` for 5% profit). If both `minimal_roi` and `custom_roi` are defined, the lower of the two thresholds will trigger an exit. For example, if `minimal_roi` is set to `{"0": 0.10}` (10% at 0 minutes) and `custom_roi` returns `0.05`, the trade will exit when the profit reaches 5%. Also, if `custom_roi` returns `0.10` and `minimal_roi` is set to `{"0": 0.05}` (5% at 0 minutes), the trade will be closed when the profit reaches 5%.
|
||||
|
||||
The method must return a float representing the new ROI threshold as a ratio, or `None` to fall back to the `minimal_roi` logic. Returning `NaN` or `inf` values is considered invalid and will be treated as `None`, causing the bot to use the `minimal_roi` configuration.
|
||||
|
||||
### Custom ROI examples
|
||||
|
||||
The following examples illustrate how to use the `custom_roi` function to implement different ROI logics.
|
||||
|
||||
#### Custom ROI per side
|
||||
|
||||
Use different ROI thresholds depending on the `side`. In this example, 5% for long entries and 2% for short entries.
|
||||
|
||||
```python
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
use_custom_roi = True
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
|
||||
entry_tag: str | None, side: str, **kwargs) -> float | None:
|
||||
"""
|
||||
Custom ROI logic, returns a new minimum ROI threshold (as a ratio, e.g., 0.05 for +5%).
|
||||
Only called when use_custom_roi is set to True.
|
||||
|
||||
If used at the same time as minimal_roi, an exit will be triggered when the lower
|
||||
threshold is reached. Example: If minimal_roi = {"0": 0.01} and custom_roi returns 0.05,
|
||||
an exit will be triggered if profit reaches 5%.
|
||||
|
||||
:param pair: Pair that's currently analyzed.
|
||||
:param trade: trade object.
|
||||
:param current_time: datetime object, containing the current datetime.
|
||||
:param trade_duration: Current trade duration in minutes.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the current trade.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New ROI value as a ratio, or None to fall back to minimal_roi logic.
|
||||
"""
|
||||
return 0.05 if side == "long" else 0.02
|
||||
```
|
||||
|
||||
#### Custom ROI per pair
|
||||
|
||||
Use different ROI thresholds depending on the `pair`.
|
||||
|
||||
```python
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
use_custom_roi = True
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
|
||||
entry_tag: str | None, side: str, **kwargs) -> float | None:
|
||||
|
||||
stake = trade.stake_currency
|
||||
roi_map = {
|
||||
f"BTC/{stake}": 0.02, # 2% for BTC
|
||||
f"ETH/{stake}": 0.03, # 3% for ETH
|
||||
f"XRP/{stake}": 0.04, # 4% for XRP
|
||||
}
|
||||
|
||||
return roi_map.get(pair, 0.01) # 1% for any other pair
|
||||
```
|
||||
|
||||
#### Custom ROI per entry tag
|
||||
|
||||
Use different ROI thresholds depending on the `entry_tag` provided with the buy signal.
|
||||
|
||||
```python
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
use_custom_roi = True
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
|
||||
entry_tag: str | None, side: str, **kwargs) -> float | None:
|
||||
|
||||
roi_by_tag = {
|
||||
"breakout": 0.08, # 8% if tag is "breakout"
|
||||
"rsi_overbought": 0.05, # 5% if tag is "rsi_overbought"
|
||||
"mean_reversion": 0.03, # 3% if tag is "mean_reversion"
|
||||
}
|
||||
|
||||
return roi_by_tag.get(entry_tag, 0.01) # 1% if tag is unknown
|
||||
```
|
||||
|
||||
#### Custom ROI based on ATR
|
||||
|
||||
ROI value may be derived from indicators stored in dataframe. This example uses the ATR ratio as ROI.
|
||||
|
||||
``` python
|
||||
# Default imports
|
||||
# <...>
|
||||
import talib.abstract as ta
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
use_custom_roi = True
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# <...>
|
||||
dataframe["atr"] = ta.ATR(dataframe, timeperiod=10)
|
||||
|
||||
def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
|
||||
entry_tag: str | None, side: str, **kwargs) -> float | None:
|
||||
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
atr_ratio = last_candle["atr"] / last_candle["close"]
|
||||
|
||||
return atr_ratio # Returns the ATR value as ratio
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom order price rules
|
||||
|
||||
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
||||
@@ -1107,3 +1242,119 @@ class AwesomeStrategy(IStrategy):
|
||||
return None
|
||||
|
||||
```
|
||||
|
||||
## Plot annotations callback
|
||||
|
||||
The plot annotations callback is called whenever freqUI requests data to display a chart.
|
||||
This callback has no meaning in the trade cycle context and is only used for charting purposes.
|
||||
|
||||
The strategy can then return a list of `AnnotationType` objects to be displayed on the chart.
|
||||
Depending on the content returned - the chart can display horizontal areas, vertical areas, or boxes.
|
||||
|
||||
The full object looks like this:
|
||||
|
||||
``` json
|
||||
{
|
||||
"type": "area", // Type of the annotation, currently only "area" is supported
|
||||
"start": "2024-01-01 15:00:00", // Start date of the area
|
||||
"end": "2024-01-01 16:00:00", // End date of the area
|
||||
"y_start": 94000.2, // Price / y axis value
|
||||
"y_end": 98000, // Price / y axis value
|
||||
"color": "",
|
||||
"label": "some label"
|
||||
}
|
||||
```
|
||||
|
||||
The below example will mark the chart with areas for the hours 8 and 15, with a grey color, highlighting the market open and close hours.
|
||||
This is obviously a very basic example.
|
||||
|
||||
``` python
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def plot_annotations(
|
||||
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
|
||||
) -> list[AnnotationType]:
|
||||
"""
|
||||
Retrieve area annotations for a chart.
|
||||
Must be returned as array, with type, label, color, start, end, y_start, y_end.
|
||||
All settings except for type are optional - though it usually makes sense to include either
|
||||
"start and end" or "y_start and y_end" for either horizontal or vertical plots
|
||||
(or all 4 for boxes).
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param start_date: Start date of the chart data being requested
|
||||
:param end_date: End date of the chart data being requested
|
||||
:param dataframe: DataFrame with the analyzed data for the chart
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return: List of AnnotationType objects
|
||||
"""
|
||||
annotations = []
|
||||
while start_dt < end_date:
|
||||
start_dt += timedelta(hours=1)
|
||||
if start_dt.hour in (8, 15):
|
||||
annotations.append(
|
||||
{
|
||||
"type": "area",
|
||||
"label": "Trade open and close hours",
|
||||
"start": start_dt,
|
||||
"end": start_dt + timedelta(hours=1),
|
||||
# Omitting y_start and y_end will result in a vertical area spanning the whole height of the main Chart
|
||||
"color": "rgba(133, 133, 133, 0.4)",
|
||||
}
|
||||
)
|
||||
|
||||
return annotations
|
||||
|
||||
```
|
||||
|
||||
Entries will be validated, and won't be passed to the UI if they don't correspond to the expected schema and will log an error if they don't.
|
||||
|
||||
!!! Warning "Many annotations"
|
||||
Using too many annotations can cause the UI to hang, especially when plotting large amounts of historic data.
|
||||
Use the annotation feature with care.
|
||||
|
||||
### Plot annotations example
|
||||
|
||||

|
||||

|
||||
|
||||
??? Info "Code used for the plot above"
|
||||
This is an example code and should be treated as such.
|
||||
|
||||
``` python
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def plot_annotations(
|
||||
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
|
||||
) -> list[AnnotationType]:
|
||||
annotations = []
|
||||
while start_dt < end_date:
|
||||
start_dt += timedelta(hours=1)
|
||||
if (start_dt.hour % 4) == 0:
|
||||
mark_areas.append(
|
||||
{
|
||||
"type": "area",
|
||||
"label": "4h",
|
||||
"start": start_dt,
|
||||
"end": start_dt + timedelta(hours=1),
|
||||
"color": "rgba(133, 133, 133, 0.4)",
|
||||
}
|
||||
)
|
||||
elif (start_dt.hour % 2) == 0:
|
||||
price = dataframe.loc[dataframe["date"] == start_dt, ["close"]].mean()
|
||||
mark_areas.append(
|
||||
{
|
||||
"type": "area",
|
||||
"label": "2h",
|
||||
"start": start_dt,
|
||||
"end": start_dt + timedelta(hours=1),
|
||||
"y_end": price * 1.01,
|
||||
"y_start": price * 0.99,
|
||||
"color": "rgba(0, 255, 0, 0.4)",
|
||||
}
|
||||
)
|
||||
|
||||
return annotations
|
||||
|
||||
```
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+1
-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
|
||||
|
||||
@@ -240,7 +240,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
|
||||
|
||||
@@ -451,21 +450,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`
|
||||
|
||||
@@ -134,15 +134,17 @@ Most properties here can be None as they are dependent on the exchange response.
|
||||
|------------|-------------|-------------|
|
||||
| `trade` | Trade | Trade object this order is attached to |
|
||||
| `ft_pair` | string | Pair this order is for |
|
||||
| `ft_is_open` | boolean | is the order filled? |
|
||||
| `ft_is_open` | boolean | is the order still open? |
|
||||
| `order_type` | string | Order type as defined on the exchange - usually market, limit or stoploss |
|
||||
| `status` | string | Status as defined by ccxt. Usually open, closed, expired or canceled |
|
||||
| `side` | string | Buy or Sell |
|
||||
| `status` | string | Status as defined by [ccxt's order structure](https://docs.ccxt.com/#/README?id=order-structure). Usually open, closed, expired, canceled or rejected |
|
||||
| `side` | string | buy or sell |
|
||||
| `price` | float | Price the order was placed at |
|
||||
| `average` | float | Average price the order filled at |
|
||||
| `amount` | float | Amount in base currency |
|
||||
| `filled` | float | Filled amount (in base currency) |
|
||||
| `remaining` | float | Remaining amount |
|
||||
| `filled` | float | Filled amount (in base currency) (use `safe_filled` instead) |
|
||||
| `safe_filled` | float | Filled amount (in base currency) - guaranteed to not be None |
|
||||
| `remaining` | float | Remaining amount (use `safe_remaining` instead) |
|
||||
| `safe_remaining` | float | Remaining amount - either taken from the exchange or calculated. |
|
||||
| `cost` | float | Cost of the order - usually average * filled (*Exchange dependent on futures, may contain the cost with or without leverage and may be in contracts.*) |
|
||||
| `stake_amount` | float | Stake amount used for this order. *Added in 2023.7.* |
|
||||
| `stake_amount_filled` | float | Filled Stake amount used for this order. *Added in 2024.11.* |
|
||||
|
||||
+1
-1
@@ -45,4 +45,4 @@ Please refer to the corresponding installation sections (common problems linked
|
||||
|
||||
Common problems and their solutions:
|
||||
|
||||
* [ta-lib update on windows](windows_installation.md#2-install-ta-lib)
|
||||
* [ta-lib update on windows](windows_installation.md#install-ta-lib)
|
||||
|
||||
+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
|
||||
|
||||
|
||||
@@ -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.10, 3.11, 3.12 and 3.13) and for 64bit Windows.
|
||||
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
|
||||
|
||||
Other versions must be downloaded from the above link.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2025.4"
|
||||
__version__ = "2025.6"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -57,6 +57,7 @@ ARGS_BACKTEST = [
|
||||
"backtest_breakdown",
|
||||
"backtest_cache",
|
||||
"freqai_backtest_live_models",
|
||||
"backtest_notes",
|
||||
]
|
||||
|
||||
ARGS_HYPEROPT = [
|
||||
@@ -78,9 +79,10 @@ ARGS_HYPEROPT = [
|
||||
"disableparamexport",
|
||||
"hyperopt_ignore_missing_space",
|
||||
"analyze_per_epoch",
|
||||
"early_stop",
|
||||
]
|
||||
|
||||
ARGS_EDGE = [*ARGS_COMMON_OPTIMIZE, "stoploss_range"]
|
||||
ARGS_EDGE = [*ARGS_COMMON_OPTIMIZE]
|
||||
|
||||
ARGS_LIST_STRATEGIES = [
|
||||
"strategy_path",
|
||||
@@ -249,7 +251,7 @@ 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"]
|
||||
@@ -504,7 +506,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",
|
||||
@@ -262,6 +260,13 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
metavar="INT",
|
||||
default=constants.HYPEROPT_EPOCH,
|
||||
),
|
||||
"early_stop": Arg(
|
||||
"--early-stop",
|
||||
help="Early stop hyperopt if no improvement after (default: %(default)d) epochs.",
|
||||
type=check_int_positive,
|
||||
metavar="INT",
|
||||
default=0, # 0 to disable by default
|
||||
),
|
||||
"spaces": Arg(
|
||||
"--spaces",
|
||||
help="Specify which parameters to hyperopt. Space-separated list.",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -26,6 +26,8 @@ __MESSAGE_TYPE_DICT: dict[str, dict[str, str]] = {x: {"type": "object"} for x in
|
||||
|
||||
__IN_STRATEGY = "\nUsually specified in the strategy and missing in the configuration."
|
||||
|
||||
__VIA_ENV = "Recommended to be set via environment variable"
|
||||
|
||||
CONF_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -421,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",
|
||||
@@ -468,11 +466,16 @@ CONF_SCHEMA = {
|
||||
},
|
||||
"token": {"description": "Telegram bot token.", "type": "string"},
|
||||
"chat_id": {
|
||||
"description": "Telegram chat or group ID",
|
||||
"description": (
|
||||
f"Telegram chat or group ID. {__VIA_ENV} FREQTRADE__TELEGRAM__CHAT_ID"
|
||||
),
|
||||
"type": "string",
|
||||
},
|
||||
"topic_id": {
|
||||
"description": "Telegram topic ID - only applicable for group chats",
|
||||
"description": (
|
||||
"Telegram topic ID - only applicable for group chats. "
|
||||
f"{__VIA_ENV} FREQTRADE__TELEGRAM__TOPIC_ID"
|
||||
),
|
||||
"type": "string",
|
||||
},
|
||||
"authorized_users": {
|
||||
@@ -574,8 +577,11 @@ CONF_SCHEMA = {
|
||||
"description": "Webhook settings.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"url": {"type": "string"},
|
||||
"enabled": {"description": "Enable webhook notifications.", "type": "boolean"},
|
||||
"url": {
|
||||
"description": f"Webhook URL. {__VIA_ENV} FREQTRADE__WEBHOOK__URL",
|
||||
"type": "string",
|
||||
},
|
||||
"format": {"type": "string", "enum": WEBHOOK_FORMAT_OPTIONS, "default": "form"},
|
||||
"retries": {"type": "integer", "minimum": 0},
|
||||
"retry_delay": {"type": "number", "minimum": 0},
|
||||
@@ -587,7 +593,12 @@ CONF_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"webhook_url": {"type": "string"},
|
||||
"webhook_url": {
|
||||
"description": (
|
||||
f"Discord webhook URL. {__VIA_ENV} FREQTRADE__DISCORD__WEBHOOK_URL"
|
||||
),
|
||||
"type": "string",
|
||||
},
|
||||
"exit_fill": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"},
|
||||
@@ -806,27 +817,57 @@ CONF_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"description": "Name of the exchange.", "type": "string"},
|
||||
"enable_ws": {
|
||||
"description": "Enable WebSocket connections to the exchange.",
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
},
|
||||
"key": {
|
||||
"description": "API key for the exchange.",
|
||||
"description": (
|
||||
f"API key for the exchange. {__VIA_ENV} FREQTRADE__EXCHANGE__KEY"
|
||||
),
|
||||
"type": "string",
|
||||
"default": "",
|
||||
},
|
||||
"secret": {
|
||||
"description": "API secret for the exchange.",
|
||||
"description": (
|
||||
f"API secret for the exchange. {__VIA_ENV} FREQTRADE__EXCHANGE__SECRET"
|
||||
),
|
||||
"type": "string",
|
||||
"default": "",
|
||||
},
|
||||
"password": {
|
||||
"description": "Password for the exchange, if required.",
|
||||
"description": (
|
||||
"Password for the exchange, if required. "
|
||||
f"{__VIA_ENV} FREQTRADE__EXCHANGE__PASSWORD"
|
||||
),
|
||||
"type": "string",
|
||||
"default": "",
|
||||
},
|
||||
"uid": {"description": "User ID for the exchange, if required.", "type": "string"},
|
||||
"uid": {
|
||||
"description": (
|
||||
"User ID for the exchange, if required. "
|
||||
f"{__VIA_ENV} FREQTRADE__EXCHANGE__UID"
|
||||
),
|
||||
"type": "string",
|
||||
},
|
||||
"account_id": {
|
||||
"description": (
|
||||
"Account ID for the exchange, if required. "
|
||||
f"{__VIA_ENV} FREQTRADE__EXCHANGE__ACCOUNT_ID"
|
||||
),
|
||||
"type": "string",
|
||||
},
|
||||
"wallet_address": {
|
||||
"description": (
|
||||
"Wallet address for the exchange, if required. "
|
||||
"Usually used by DEX exchanges. "
|
||||
f"{__VIA_ENV} FREQTRADE__EXCHANGE__WALLET_ADDRESS"
|
||||
),
|
||||
"type": "string",
|
||||
},
|
||||
"private_key": {
|
||||
"description": (
|
||||
"Private key for the exchange, if required. Usually used by DEX exchanges. "
|
||||
f"{__VIA_ENV} FREQTRADE__EXCHANGE__PRIVATE_KEY"
|
||||
),
|
||||
"type": "string",
|
||||
},
|
||||
"pair_whitelist": {
|
||||
"description": "List of whitelisted trading pairs.",
|
||||
"type": "array",
|
||||
@@ -847,6 +888,11 @@ CONF_SCHEMA = {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"enable_ws": {
|
||||
"description": "Enable WebSocket connections to the exchange.",
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
},
|
||||
"unknown_fee_rate": {
|
||||
"description": "Fee rate for unknown markets.",
|
||||
"type": "number",
|
||||
@@ -863,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,26 +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",
|
||||
]
|
||||
config = deepcopy(config)
|
||||
for key in keys_to_remove:
|
||||
for key in _SENSITIVE_KEYS:
|
||||
if "." in key:
|
||||
nested_keys = key.split(".")
|
||||
nested_config = config
|
||||
@@ -44,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")
|
||||
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
|
||||
@@ -310,17 +309,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 = [
|
||||
@@ -334,6 +326,19 @@ class Configuration:
|
||||
("print_all", "Parameter --print-all detected ..."),
|
||||
]
|
||||
self._args_to_config_loop(config, configurations)
|
||||
es_epochs = self.args.get("early_stop", 0)
|
||||
if es_epochs > 0:
|
||||
if es_epochs < 20:
|
||||
logger.warning(
|
||||
f"Early stop epochs {es_epochs} lower than 20. It will be replaced with 20."
|
||||
)
|
||||
config.update({"early_stop": 20})
|
||||
else:
|
||||
config.update({"early_stop": self.args["early_stop"]})
|
||||
logger.info(
|
||||
f"Parameter --early-stop detected ... Will early stop hyperopt if no improvement "
|
||||
f"after {config.get('early_stop')} epochs ..."
|
||||
)
|
||||
|
||||
configurations = [
|
||||
("print_json", "Parameter --print-json detected ..."),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -71,6 +71,19 @@ DEFAULT_DATAFRAME_COLUMNS = ["date", "open", "high", "low", "close", "volume"]
|
||||
# it has wide consequences for stored trades files
|
||||
DEFAULT_TRADES_COLUMNS = ["timestamp", "id", "type", "side", "price", "amount", "cost"]
|
||||
DEFAULT_ORDERFLOW_COLUMNS = ["level", "bid", "ask", "delta"]
|
||||
ORDERFLOW_ADDED_COLUMNS = [
|
||||
"trades",
|
||||
"orderflow",
|
||||
"imbalances",
|
||||
"stacked_imbalances_bid",
|
||||
"stacked_imbalances_ask",
|
||||
"max_delta",
|
||||
"min_delta",
|
||||
"bid",
|
||||
"ask",
|
||||
"delta",
|
||||
"total_trades",
|
||||
]
|
||||
TRADES_DTYPES = {
|
||||
"timestamp": "int64",
|
||||
"id": "str",
|
||||
|
||||
@@ -25,6 +25,7 @@ from .bt_fileutils import (
|
||||
trade_list_to_dataframe,
|
||||
update_backtest_metadata,
|
||||
)
|
||||
from .historic_precision import get_tick_size_over_time
|
||||
from .trade_parallelism import (
|
||||
analyze_trade_parallelism,
|
||||
evaluate_result_multi,
|
||||
|
||||
@@ -52,6 +52,7 @@ BT_DATA_COLUMNS = [
|
||||
"open_timestamp",
|
||||
"close_timestamp",
|
||||
"orders",
|
||||
"funding_fees",
|
||||
]
|
||||
|
||||
|
||||
@@ -356,6 +357,8 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
|
||||
df["max_stake_amount"] = df["stake_amount"]
|
||||
if "orders" not in df.columns:
|
||||
df["orders"] = None
|
||||
if "funding_fees" not in df.columns:
|
||||
df["funding_fees"] = 0.0
|
||||
return df
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from pandas import DataFrame, Series
|
||||
|
||||
|
||||
def get_tick_size_over_time(candles: DataFrame) -> Series:
|
||||
"""
|
||||
Calculate the number of significant digits for candles over time.
|
||||
It's using the Monthly maximum of the number of significant digits for each month.
|
||||
:param candles: DataFrame with OHLCV data
|
||||
:return: Series with the average number of significant digits for each month
|
||||
"""
|
||||
# count the number of significant digits for the open and close prices
|
||||
for col in ["open", "high", "low", "close"]:
|
||||
candles[f"{col}_count"] = (
|
||||
candles[col].round(14).astype(str).str.extract(r"\.(\d*[1-9])")[0].str.len()
|
||||
)
|
||||
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
|
||||
axis=1
|
||||
)
|
||||
|
||||
candles1 = candles.set_index("date", drop=True)
|
||||
# Group by month and calculate the average number of significant digits
|
||||
monthly_count_avg1 = candles1["max_count"].resample("MS").max()
|
||||
# monthly_open_count_avg
|
||||
# convert monthly_open_count_avg from 5.0 to 0.00001, 4.0 to 0.0001, ...
|
||||
monthly_open_count_avg = 1 / 10**monthly_count_avg1
|
||||
|
||||
return monthly_open_count_avg
|
||||
@@ -9,26 +9,12 @@ from datetime import datetime
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import DEFAULT_ORDERFLOW_COLUMNS, Config
|
||||
from freqtrade.constants import DEFAULT_ORDERFLOW_COLUMNS, ORDERFLOW_ADDED_COLUMNS, Config
|
||||
from freqtrade.exceptions import DependencyException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ORDERFLOW_ADDED_COLUMNS = [
|
||||
"trades",
|
||||
"orderflow",
|
||||
"imbalances",
|
||||
"stacked_imbalances_bid",
|
||||
"stacked_imbalances_ask",
|
||||
"max_delta",
|
||||
"min_delta",
|
||||
"bid",
|
||||
"ask",
|
||||
"delta",
|
||||
"total_trades",
|
||||
]
|
||||
|
||||
|
||||
def _init_dataframe_with_trades_columns(dataframe: pd.DataFrame):
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -405,7 +405,7 @@ class DataProvider:
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,19 +32,23 @@ class Binance(Exchange):
|
||||
"stop_price_param": "stopPrice",
|
||||
"stop_price_prop": "stopPrice",
|
||||
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||
"stoploss_blocks_assets": True, # By default stoploss orders block assets
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "fromId",
|
||||
"trades_has_history": True,
|
||||
"fetch_orders_limit_minutes": None,
|
||||
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
||||
"ws_enabled": True,
|
||||
}
|
||||
_ft_has_futures: FtHas = {
|
||||
"funding_fee_candle_limit": 1000,
|
||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC"],
|
||||
"tickers_have_price": False,
|
||||
"floor_leverage": True,
|
||||
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
|
||||
"stop_price_type_field": "workingType",
|
||||
"order_props_in_contracts": ["amount", "cost", "filled", "remaining"],
|
||||
"stop_price_type_value_mapping": {
|
||||
@@ -72,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(
|
||||
@@ -400,7 +407,7 @@ class Binance(Exchange):
|
||||
since = max(since, listing_date)
|
||||
|
||||
_, res = await download_archive_trades(
|
||||
CandleType.SPOT,
|
||||
CandleType.FUTURES if self.trading_mode == "futures" else CandleType.SPOT,
|
||||
pair,
|
||||
since_ms=since,
|
||||
until_ms=until,
|
||||
|
||||
+45574
-32481
File diff suppressed because it is too large
Load Diff
+17
-20
@@ -12,7 +12,6 @@ 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.util.datetime_helpers import dt_now, dt_ts
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -35,6 +34,7 @@ class Bybit(Exchange):
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||
"ws_enabled": True,
|
||||
"trades_has_history": False, # Endpoint doesn't support pagination
|
||||
"fetch_orders_limit_minutes": 7 * 1440, # 7 days
|
||||
"exchange_has_overrides": {
|
||||
# Bybit spot does not support fetch_order
|
||||
# Unless the account is unified.
|
||||
@@ -49,6 +49,7 @@ class Bybit(Exchange):
|
||||
"funding_fee_candle_limit": 200,
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||
"stoploss_blocks_assets": False,
|
||||
# bybit response parsing fails to populate stopLossPrice
|
||||
"stop_price_prop": "stopPrice",
|
||||
"stop_price_type_field": "triggerBy",
|
||||
@@ -139,6 +140,21 @@ class Bybit(Exchange):
|
||||
params["position_idx"] = 0
|
||||
return params
|
||||
|
||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> dict:
|
||||
params = super()._get_stop_params(
|
||||
side=side,
|
||||
ordertype=ordertype,
|
||||
stop_price=stop_price,
|
||||
)
|
||||
# work around ccxt bug introduced in https://github.com/ccxt/ccxt/pull/25887
|
||||
# Where create_order ain't returning an ID any longer.
|
||||
params.update(
|
||||
{
|
||||
"method": "privatePostV5OrderCreate",
|
||||
}
|
||||
)
|
||||
return params
|
||||
|
||||
def _order_needs_price(self, side: BuySell, ordertype: str) -> bool:
|
||||
# Bybit requires price for market orders - but only for classic accounts,
|
||||
# and only in spot mode
|
||||
@@ -234,25 +250,6 @@ class Bybit(Exchange):
|
||||
logger.warning(f"Could not update funding fees for {pair}.")
|
||||
return 0.0
|
||||
|
||||
def fetch_orders(
|
||||
self, pair: str, since: datetime, params: dict | None = None
|
||||
) -> list[CcxtOrder]:
|
||||
"""
|
||||
Fetch all orders for a pair "since"
|
||||
:param pair: Pair for the query
|
||||
:param since: Starting time for the query
|
||||
"""
|
||||
# On bybit, the distance between since and "until" can't exceed 7 days.
|
||||
# we therefore need to split the query into multiple queries.
|
||||
orders = []
|
||||
|
||||
while since < dt_now():
|
||||
until = since + timedelta(days=7, minutes=-1)
|
||||
orders += super().fetch_orders(pair, since, params={"until": dt_ts(until)})
|
||||
since = until
|
||||
|
||||
return orders
|
||||
|
||||
def fetch_order(self, order_id: str, pair: str, params: dict | None = None) -> CcxtOrder:
|
||||
if self.exchange_has("fetchOrder"):
|
||||
# Set acknowledged to True to avoid ccxt exception
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -131,6 +131,7 @@ class Exchange:
|
||||
"stop_price_param": "stopLossPrice", # Used for stoploss_on_exchange request
|
||||
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
|
||||
"stoploss_order_types": {},
|
||||
"stoploss_blocks_assets": True, # By default stoploss orders block assets
|
||||
"order_time_in_force": ["GTC"],
|
||||
"ohlcv_params": {},
|
||||
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
||||
@@ -155,6 +156,7 @@ class Exchange:
|
||||
"ccxt_futures_name": "swap",
|
||||
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
||||
"order_props_in_contracts": ["amount", "filled", "remaining"],
|
||||
"fetch_orders_limit_minutes": None, # "fetch_orders" is not time-limited by default
|
||||
# Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
|
||||
"marketOrderRequiresPrice": False,
|
||||
"exchange_has_overrides": {}, # Dictionary overriding ccxt's "has".
|
||||
@@ -267,11 +269,11 @@ class Exchange:
|
||||
exchange_conf.get("ccxt_async_config", {}), ccxt_async_config
|
||||
)
|
||||
self._api_async = self._init_ccxt(exchange_conf, False, ccxt_async_config)
|
||||
self._has_watch_ohlcv = self.exchange_has("watchOHLCV") and self._ft_has["ws_enabled"]
|
||||
_has_watch_ohlcv = self.exchange_has("watchOHLCV") and self._ft_has["ws_enabled"]
|
||||
if (
|
||||
self._config["runmode"] in TRADE_MODES
|
||||
and exchange_conf.get("enable_ws", True)
|
||||
and self._has_watch_ohlcv
|
||||
and _has_watch_ohlcv
|
||||
):
|
||||
self._ws_async = self._init_ccxt(exchange_conf, False, ccxt_async_config)
|
||||
self._exchange_ws = ExchangeWS(self._config, self._ws_async)
|
||||
@@ -635,9 +637,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:
|
||||
@@ -647,14 +649,14 @@ 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
|
||||
return None
|
||||
except asyncio.TimeoutError as e:
|
||||
logger.warning("Could not load markets. Reason: %s", e)
|
||||
raise TemporaryError from e
|
||||
@@ -677,7 +679,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
|
||||
@@ -874,8 +877,8 @@ 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 get_option(self, param: str, default: Any | None = None) -> Any:
|
||||
@@ -1742,7 +1745,7 @@ class Exchange:
|
||||
return orders
|
||||
|
||||
@retrier(retries=0)
|
||||
def fetch_orders(
|
||||
def _fetch_orders(
|
||||
self, pair: str, since: datetime, params: dict | None = None
|
||||
) -> list[CcxtOrder]:
|
||||
"""
|
||||
@@ -1781,6 +1784,24 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def fetch_orders(
|
||||
self, pair: str, since: datetime, params: dict | None = None
|
||||
) -> list[CcxtOrder]:
|
||||
if self._config["dry_run"]:
|
||||
return []
|
||||
if (limit := self._ft_has.get("fetch_orders_limit_minutes")) is not None:
|
||||
orders = []
|
||||
while since < dt_now():
|
||||
orders += self._fetch_orders(pair, since)
|
||||
# Since with 1 minute overlap
|
||||
since = since + timedelta(minutes=limit - 1)
|
||||
# Ensure each order is unique based on order id
|
||||
orders = list({order["id"]: order for order in orders}.values())
|
||||
return orders
|
||||
|
||||
else:
|
||||
return self._fetch_orders(pair, since, params=params)
|
||||
|
||||
@retrier
|
||||
def fetch_trading_fees(self) -> dict[str, Any]:
|
||||
"""
|
||||
@@ -2414,6 +2435,53 @@ class Exchange:
|
||||
data = sorted(data, key=lambda x: x[0])
|
||||
return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
|
||||
|
||||
def _try_build_from_websocket(
|
||||
self, pair: str, timeframe: str, candle_type: CandleType
|
||||
) -> Coroutine[Any, Any, OHLCVResponse] | None:
|
||||
"""
|
||||
Try to build a coroutine to get data from websocket.
|
||||
"""
|
||||
if self._can_use_websocket(self._exchange_ws, pair, timeframe, candle_type):
|
||||
candle_ts = dt_ts(timeframe_to_prev_date(timeframe))
|
||||
prev_candle_ts = dt_ts(date_minus_candles(timeframe, 1))
|
||||
candles = self._exchange_ws.ohlcvs(pair, timeframe)
|
||||
half_candle = int(candle_ts - (candle_ts - prev_candle_ts) * 0.5)
|
||||
last_refresh_time = int(
|
||||
self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0)
|
||||
)
|
||||
|
||||
if (
|
||||
candles
|
||||
and (
|
||||
(len(candles) > 1 and candles[-1][0] >= prev_candle_ts)
|
||||
# Edgecase on reconnect, where 1 candle is available but it's the current one
|
||||
or (len(candles) == 1 and candles[-1][0] < candle_ts)
|
||||
)
|
||||
and last_refresh_time >= half_candle
|
||||
):
|
||||
# Usable result, candle contains the previous candle.
|
||||
# Also, we check if the last refresh time is no more than half the candle ago.
|
||||
logger.debug(f"reuse watch result for {pair}, {timeframe}, {last_refresh_time}")
|
||||
|
||||
return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_ts)
|
||||
logger.info(
|
||||
f"Couldn't reuse watch for {pair}, {timeframe}, falling back to REST api. "
|
||||
f"{candle_ts < last_refresh_time}, {candle_ts}, {last_refresh_time}, "
|
||||
f"{format_ms_time(candle_ts)}, {format_ms_time(last_refresh_time)} "
|
||||
)
|
||||
return None
|
||||
|
||||
def _can_use_websocket(
|
||||
self, exchange_ws: ExchangeWS | None, pair: str, timeframe: str, candle_type: CandleType
|
||||
) -> TypeGuard[ExchangeWS]:
|
||||
"""
|
||||
Check if we can use websocket for this pair.
|
||||
Acts as typeguard for exchangeWs
|
||||
"""
|
||||
if exchange_ws and candle_type in (CandleType.SPOT, CandleType.FUTURES):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _build_coroutine(
|
||||
self,
|
||||
pair: str,
|
||||
@@ -2423,8 +2491,8 @@ class Exchange:
|
||||
cache: bool,
|
||||
) -> Coroutine[Any, Any, OHLCVResponse]:
|
||||
not_all_data = cache and self.required_candle_call_count > 1
|
||||
if cache and candle_type in (CandleType.SPOT, CandleType.FUTURES):
|
||||
if self._has_watch_ohlcv and self._exchange_ws:
|
||||
if cache:
|
||||
if self._can_use_websocket(self._exchange_ws, pair, timeframe, candle_type):
|
||||
# Subscribe to websocket
|
||||
self._exchange_ws.schedule_ohlcv(pair, timeframe, candle_type)
|
||||
|
||||
@@ -2432,30 +2500,9 @@ class Exchange:
|
||||
candle_limit = self.ohlcv_candle_limit(timeframe, candle_type)
|
||||
min_ts = dt_ts(date_minus_candles(timeframe, candle_limit - 5))
|
||||
|
||||
if self._exchange_ws:
|
||||
candle_ts = dt_ts(timeframe_to_prev_date(timeframe))
|
||||
prev_candle_ts = dt_ts(date_minus_candles(timeframe, 1))
|
||||
candles = self._exchange_ws.ohlcvs(pair, timeframe)
|
||||
half_candle = int(candle_ts - (candle_ts - prev_candle_ts) * 0.5)
|
||||
last_refresh_time = int(
|
||||
self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0)
|
||||
)
|
||||
|
||||
if (
|
||||
candles
|
||||
and candles[-1][0] >= prev_candle_ts
|
||||
and last_refresh_time >= half_candle
|
||||
):
|
||||
# Usable result, candle contains the previous candle.
|
||||
# Also, we check if the last refresh time is no more than half the candle ago.
|
||||
logger.debug(f"reuse watch result for {pair}, {timeframe}, {last_refresh_time}")
|
||||
|
||||
return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_ts)
|
||||
logger.info(
|
||||
f"Couldn't reuse watch for {pair}, {timeframe}, falling back to REST api. "
|
||||
f"{candle_ts < last_refresh_time}, {candle_ts}, {last_refresh_time}, "
|
||||
f"{format_ms_time(candle_ts)}, {format_ms_time(last_refresh_time)} "
|
||||
)
|
||||
if ws_resp := self._try_build_from_websocket(pair, timeframe, candle_type):
|
||||
# We have a usable websocket response
|
||||
return ws_resp
|
||||
|
||||
# Check if 1 call can get us updated candles without hole in the data.
|
||||
if min_ts < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0):
|
||||
@@ -3382,7 +3429,8 @@ class Exchange:
|
||||
raise InvalidOrderException(f"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
|
||||
|
||||
@@ -15,6 +15,7 @@ class FtHas(TypedDict, total=False):
|
||||
stop_price_type_field: str
|
||||
stop_price_type_value_mapping: dict
|
||||
stoploss_order_types: dict[str, str]
|
||||
stoploss_blocks_assets: bool
|
||||
# ohlcv
|
||||
ohlcv_params: dict
|
||||
ohlcv_candle_limit: int
|
||||
@@ -38,6 +39,8 @@ class FtHas(TypedDict, total=False):
|
||||
l2_limit_range: list[int] | None
|
||||
l2_limit_range_required: bool
|
||||
l2_limit_upper: int | None
|
||||
# fetch_orders
|
||||
fetch_orders_limit_minutes: int | None
|
||||
# Futures
|
||||
ccxt_futures_name: str # usually swap
|
||||
mark_ohlcv_price: str
|
||||
|
||||
@@ -46,6 +46,7 @@ class Gate(Exchange):
|
||||
"funding_fee_candle_limit": 90,
|
||||
"stop_price_type_field": "price_type",
|
||||
"l2_limit_upper": 300,
|
||||
"stoploss_blocks_assets": False,
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: 0,
|
||||
PriceType.MARK: 1,
|
||||
@@ -69,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")
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ class Hyperliquid(Exchange):
|
||||
_ft_has_futures: FtHas = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stoploss_blocks_assets": False,
|
||||
"stop_price_prop": "stopPrice",
|
||||
"funding_fee_timeframe": "1h",
|
||||
"funding_fee_candle_limit": 500,
|
||||
|
||||
@@ -12,7 +12,7 @@ from freqtrade.enums import MarginMode, TradingMode
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_types import CcxtBalances, FtHas, Tickers
|
||||
from freqtrade.exchange.exchange_types import CcxtBalances, FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -49,18 +49,6 @@ class Kraken(Exchange):
|
||||
|
||||
return parent_check and market.get("darkpool", False) is False
|
||||
|
||||
def get_tickers(
|
||||
self,
|
||||
symbols: list[str] | None = None,
|
||||
*,
|
||||
cached: bool = False,
|
||||
market_type: TradingMode | None = None,
|
||||
) -> Tickers:
|
||||
# Only fetch tickers for current stake currency
|
||||
# Otherwise the request for kraken becomes too large.
|
||||
symbols = list(self.get_markets(quote_currencies=[self._config["stake_currency"]]))
|
||||
return super().get_tickers(symbols=symbols, cached=cached, market_type=market_type)
|
||||
|
||||
def consolidate_balances(self, balances: CcxtBalances) -> CcxtBalances:
|
||||
"""
|
||||
Consolidate balances for the same currency.
|
||||
|
||||
@@ -44,6 +44,7 @@ class Okx(Exchange):
|
||||
PriceType.MARK: "index",
|
||||
PriceType.INDEX: "mark",
|
||||
},
|
||||
"stoploss_blocks_assets": False,
|
||||
"ws_enabled": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -318,13 +318,13 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
||||
rename_dict = {
|
||||
"%-raw_open": "open",
|
||||
"%-raw_low": "low",
|
||||
"%-raw_high": " high",
|
||||
"%-raw_high": "high",
|
||||
"%-raw_close": "close",
|
||||
}
|
||||
rename_dict_old = {
|
||||
f"%-{pair}raw_open_{tf}": "open",
|
||||
f"%-{pair}raw_low_{tf}": "low",
|
||||
f"%-{pair}raw_high_{tf}": " high",
|
||||
f"%-{pair}raw_high_{tf}": "high",
|
||||
f"%-{pair}raw_close_{tf}": "close",
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from pandas import DataFrame
|
||||
from sklearn.model_selection import train_test_split
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DOCS_LINK, Config
|
||||
from freqtrade.constants import DOCS_LINK, ORDERFLOW_ADDED_COLUMNS, Config
|
||||
from freqtrade.data.converter import reduce_dataframe_footprint
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
@@ -709,6 +709,11 @@ class FreqaiDataKitchen:
|
||||
skip_columns = [
|
||||
(f"{s}_{suffix}") for s in ["date", "open", "high", "low", "close", "volume"]
|
||||
]
|
||||
|
||||
for s in ORDERFLOW_ADDED_COLUMNS:
|
||||
if s in dataframe.columns and f"{s}_{suffix}" in dataframe.columns:
|
||||
skip_columns.append(f"{s}_{suffix}")
|
||||
|
||||
dataframe = dataframe.drop(columns=skip_columns)
|
||||
return dataframe
|
||||
|
||||
|
||||
@@ -763,6 +763,8 @@ class IFreqaiModel(ABC):
|
||||
init_model = None
|
||||
else:
|
||||
init_model = self.dd.model_dictionary[pair]
|
||||
# Set "fresh" tb_logger - the one in model_dictionary has the writer closed.
|
||||
init_model.tb_logger = self.tb_logger
|
||||
|
||||
return init_model
|
||||
|
||||
|
||||
@@ -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]
|
||||
"""
|
||||
|
||||
+25
-47
@@ -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,
|
||||
@@ -131,13 +129,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 +233,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
|
||||
@@ -335,7 +325,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 +333,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
|
||||
@@ -467,7 +452,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
order = trade.select_order("stoploss", False)
|
||||
if order:
|
||||
logger.info(
|
||||
f"Updating {trade.exit_side}-fee on trade {trade}"
|
||||
f"Updating {trade.exit_side}-fee on trade {trade} "
|
||||
f"for order {order.order_id}."
|
||||
)
|
||||
self.update_trade_state(
|
||||
@@ -485,7 +470,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
open_order = trade.select_order(trade.entry_side, True)
|
||||
if order and open_order is None:
|
||||
logger.info(
|
||||
f"Updating {trade.entry_side}-fee on trade {trade}"
|
||||
f"Updating {trade.entry_side}-fee on trade {trade} "
|
||||
f"for order {order.order_id}."
|
||||
)
|
||||
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||
@@ -701,9 +686,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 (
|
||||
@@ -1042,13 +1025,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)
|
||||
@@ -1170,7 +1151,7 @@ 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
|
||||
@@ -1299,6 +1280,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(
|
||||
@@ -1382,7 +1364,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
datetime.now(timezone.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:
|
||||
@@ -1476,20 +1458,17 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.handle_protections(trade.pair, trade.trade_direction)
|
||||
return True
|
||||
|
||||
if not trade.has_open_position or not trade.is_open:
|
||||
if (
|
||||
not trade.has_open_position
|
||||
or not trade.is_open
|
||||
or (trade.has_open_orders and self.exchange.get_option("stoploss_blocks_assets", True))
|
||||
):
|
||||
# The trade can be closed already (sell-order fill confirmation came in this iteration)
|
||||
return False
|
||||
|
||||
# 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.
|
||||
@@ -2364,12 +2343,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
# 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)
|
||||
@@ -2472,10 +2452,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)
|
||||
@@ -2492,9 +2471,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
|
||||
|
||||
|
||||
@@ -7,4 +7,5 @@ from freqtrade.ft_types.backtest_result_type import (
|
||||
BacktestResultType,
|
||||
get_BacktestResultType_default,
|
||||
)
|
||||
from freqtrade.ft_types.plot_annotation_type import AnnotationType
|
||||
from freqtrade.ft_types.valid_exchanges_type import ValidExchangesType
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
from typing_extensions import Required, TypedDict
|
||||
|
||||
|
||||
class AnnotationType(TypedDict, total=False):
|
||||
type: Required[Literal["area"]]
|
||||
start: str | datetime
|
||||
end: str | datetime
|
||||
y_start: float
|
||||
y_end: float
|
||||
color: str
|
||||
label: str
|
||||
|
||||
|
||||
AnnotationTypeTA = TypeAdapter(AnnotationType)
|
||||
@@ -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)
|
||||
|
||||
@@ -129,7 +129,6 @@ class LookaheadAnalysis(BaseAnalysis):
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
|
||||
varholder.data, varholder.timerange = backtesting.load_bt_data()
|
||||
backtesting.load_bt_data_detail()
|
||||
varholder.timeframe = backtesting.timeframe
|
||||
|
||||
varholder.indicators = backtesting.strategy.advise_all_indicators(varholder.data)
|
||||
@@ -236,7 +235,7 @@ class LookaheadAnalysis(BaseAnalysis):
|
||||
return None
|
||||
if "force_exit" in result_row["exit_reason"]:
|
||||
logger.info(
|
||||
"found force-exit in pair: {result_row['pair']}, "
|
||||
f"found force-exit in pair: {result_row['pair']}, "
|
||||
f"timerange:{result_row['open_date']}-{result_row['close_date']}, "
|
||||
f"idx: {idx}, skipping this one to avoid a false-positive."
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user