Compare commits
582 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc2abe32a3 | |||
| 43bb0f7cb1 | |||
| 9ee87cb6ba | |||
| 0ed613a574 | |||
| c7dfd54eb3 | |||
| 697814ed6a | |||
| d5417d40ae | |||
| e386334130 | |||
| 57e489b9aa | |||
| e0d55bf5d3 | |||
| f169f99a5f | |||
| 0982b26d6b | |||
| 626e719942 | |||
| d82cac3895 | |||
| 3e40944b4e | |||
| 05081df48b | |||
| 6e6082fa2b | |||
| 3c43e3dae3 | |||
| 515a754dc4 | |||
| 1c98c2feeb | |||
| 697059a4a2 | |||
| 1b1508d8f2 | |||
| 48905e55e5 | |||
| a203f4129a | |||
| 0b3f3a94b5 | |||
| daf9918bb5 | |||
| 3bde73740c | |||
| 3f9eaba1ef | |||
| d557b4b298 | |||
| fd858b79af | |||
| e96c67e140 | |||
| faf28180a5 | |||
| e98959f3f0 | |||
| 0a2c3379c6 | |||
| b8f5df7cf6 | |||
| 04a4a97efb | |||
| 143d9a90c6 | |||
| c112fa6632 | |||
| cb46f72109 | |||
| 2eb5efed64 | |||
| 7657e37f67 | |||
| ec763e684f | |||
| d3847080e8 | |||
| 97305221ac | |||
| b65aa8671a | |||
| d35bc15562 | |||
| a9cabe1162 | |||
| bb6f4eb60d | |||
| cea1700eaa | |||
| bb2f4ac37e | |||
| 996123ea52 | |||
| 252b7b5e68 | |||
| fefba87922 | |||
| 434911c1cf | |||
| 1d947ba21a | |||
| 55a6591f10 | |||
| 266fe5c6cb | |||
| 3d4395ebe6 | |||
| 463f0050ed | |||
| b0b9c8d43a | |||
| 94d0080208 | |||
| 250156f1a3 | |||
| 724318c43e | |||
| 37dbaae8aa | |||
| 1bd0404503 | |||
| eba9c24510 | |||
| 6bf1901f9b | |||
| bd9697e6be | |||
| 200b9da2a2 | |||
| 32fecd7b15 | |||
| f8b99c6e66 | |||
| 0fda77a39c | |||
| 57c7edfe26 | |||
| 437d0a264c | |||
| 3a28c91635 | |||
| 1f4c188dbf | |||
| 7f9b1a9f90 | |||
| 5096ec8a50 | |||
| 35d812a271 | |||
| 1e79c1961b | |||
| bfd758d353 | |||
| b1ef0bf58d | |||
| d25b2ea10a | |||
| c533e48310 | |||
| a1e9b6bea8 | |||
| 0dfd7324bd | |||
| 3c4b07ce46 | |||
| d6e6c9d038 | |||
| 027ac3d66c | |||
| 8c1674b415 | |||
| eaa35d7b47 | |||
| 0dc26584cc | |||
| cb2e50abcf | |||
| f0aedc8d23 | |||
| 086e1d8bad | |||
| 83c2ca5a90 | |||
| 68c8dd4e18 | |||
| b25defb8b9 | |||
| b190079aa8 | |||
| a831a6ee34 | |||
| bfbae5d040 | |||
| bb2f19deaa | |||
| cfa4924b8e | |||
| 4f8291384d | |||
| f81e335b27 | |||
| 756f178ec3 | |||
| c68c6896d7 | |||
| 712fd8ac14 | |||
| cb6a1267b6 | |||
| 1c77bd6f5d | |||
| 1d0bcbe234 | |||
| 445a22222a | |||
| ba80329eda | |||
| a8d7a72cec | |||
| 6ed470884b | |||
| 0d4dadd63f | |||
| 0cab0e8ac5 | |||
| 9ddf464c03 | |||
| 06abc95b2e | |||
| b4daabe491 | |||
| e5a23f5afd | |||
| 7ee4338b1f | |||
| 3f6e7ca4c1 | |||
| 1345cfdbee | |||
| f2780c7c68 | |||
| 1254bd5bab | |||
| cef63725aa | |||
| 5af4147eb3 | |||
| ca9d4ed026 | |||
| 15359f57a8 | |||
| 4b23e9ccce | |||
| 35706eb9ec | |||
| 272771af38 | |||
| 2ed3db330a | |||
| 38d197b3bf | |||
| 6c588cce65 | |||
| 5752e52b0f | |||
| 2077f34a78 | |||
| 7718e8e1e9 | |||
| 06e2b8ae0d | |||
| 42361bc046 | |||
| 507ea87e9f | |||
| 60f71bbc2b | |||
| ea5ae9627d | |||
| 6fdeab2747 | |||
| 04ec24e737 | |||
| d9629c4d67 | |||
| 2f7e77a41e | |||
| 46e96e20a9 | |||
| 1a5f4127e0 | |||
| 51588c2ef1 | |||
| 271ee6d36e | |||
| 72995835ea | |||
| 1922f15735 | |||
| 633ff8c98c | |||
| 10b2a27342 | |||
| 6ed329bac3 | |||
| cebbb40c99 | |||
| adec491b3b | |||
| cbeae975f0 | |||
| a2b1c3710f | |||
| bbb1fecb02 | |||
| b7c89a72fa | |||
| 0a7fe730f0 | |||
| cadb269b58 | |||
| 168962f847 | |||
| e70e56df96 | |||
| 3242be2e29 | |||
| 7fc6463900 | |||
| 9560ba2144 | |||
| 257e26f2c0 | |||
| 27a650cf1c | |||
| 5dca913b9e | |||
| 6492f3d29e | |||
| 21c5cc7994 | |||
| 056bb213f8 | |||
| 5d3c65f7da | |||
| 5061554ada | |||
| 14e97e3f22 | |||
| 0f816ddfb7 | |||
| 92dd17ab62 | |||
| a8c887a5b0 | |||
| 009fa747e3 | |||
| 2744449773 | |||
| 537426d77a | |||
| e3994d1add | |||
| 720cd8b68c | |||
| 3846bd16f0 | |||
| 7863d4b854 | |||
| 7e8da54ad1 | |||
| ae06f90ca6 | |||
| 8a7b235b6e | |||
| daa0361c02 | |||
| 9136877bc7 | |||
| 2df3890917 | |||
| f2b930079b | |||
| f0a83c9e59 | |||
| b12ea975e0 | |||
| ab74dd785c | |||
| 39affd30f9 | |||
| ff6c96022e | |||
| fc3b8f37b0 | |||
| 26615dbd55 | |||
| 6411a76f03 | |||
| ce1734b176 | |||
| 2b3e3a5c46 | |||
| 0eddda2ee1 | |||
| 05ffc2dba0 | |||
| ec1aee1f85 | |||
| 9524fc0f81 | |||
| 3d56701e20 | |||
| d8d86ce861 | |||
| 503dfb7313 | |||
| 16717df1a9 | |||
| 6713cda109 | |||
| 0652339356 | |||
| 59a81dc07a | |||
| fcecbe780b | |||
| be00f2c26f | |||
| 10ba9e3105 | |||
| 1498860530 | |||
| 902bc75bf3 | |||
| bd6f891b07 | |||
| 36e259b347 | |||
| f1aaf99317 | |||
| 459a0decbd | |||
| 71598294a1 | |||
| e28eedaf1c | |||
| 3cd8b2e841 | |||
| 9925c4ac14 | |||
| 4170c943e7 | |||
| 70fe211a8b | |||
| 23a17859b1 | |||
| c1fa259ad7 | |||
| 4a9a630765 | |||
| a1cbfce316 | |||
| 5edfa675ef | |||
| 3158adad03 | |||
| ebf39b35b8 | |||
| 756b956a77 | |||
| b46ff76d58 | |||
| 6f039fa27c | |||
| acf3559205 | |||
| c05fe50b47 | |||
| a51ba0a2b4 | |||
| 9da9250865 | |||
| 881edddb6e | |||
| 0bd5086a0f | |||
| e563e07d40 | |||
| f34ad5fc42 | |||
| 6b6dcd8c52 | |||
| d5fa99696d | |||
| 2d75f002f9 | |||
| c31894be72 | |||
| 4d254952e3 | |||
| 24c57b5356 | |||
| 186e547ce1 | |||
| 555f653440 | |||
| 8ef5f54e61 | |||
| 6576528aad | |||
| 052e6f7865 | |||
| 560cb49d66 | |||
| c93ca48d17 | |||
| c2d682a5ed | |||
| 1e7c9cc285 | |||
| f0a9a6a7c0 | |||
| 47433ebe67 | |||
| e2034ed286 | |||
| cd5b9a0d56 | |||
| 8dd3fdafa9 | |||
| 6d6a9619a3 | |||
| a097727e32 | |||
| 14f3d7c819 | |||
| 7492f6d34e | |||
| 7a69e85cd6 | |||
| 27a39a4ec6 | |||
| 5e520136be | |||
| c6c30135c3 | |||
| 6f06569a11 | |||
| f3f6c7b73c | |||
| 958a9307fc | |||
| c49dc36a23 | |||
| 6cf73786e3 | |||
| 71f03ffbc6 | |||
| b57a7f207e | |||
| b283cabbcd | |||
| 38e64e06d1 | |||
| b35082e45e | |||
| 02475ebcce | |||
| d584544a6a | |||
| 428d1397a7 | |||
| aabda8d4f1 | |||
| 9cee5cc4d0 | |||
| 9dd2a9e5f5 | |||
| 2832c6b858 | |||
| bc0aeb367a | |||
| 1e33ea0059 | |||
| 0ca49bf1d3 | |||
| c1c2d24bdc | |||
| bbba9579dc | |||
| f92c66db56 | |||
| f08874d9b8 | |||
| da0e3912c6 | |||
| e15cd81383 | |||
| d1a17a2508 | |||
| da3c44137a | |||
| 192bd754df | |||
| ec46299ec0 | |||
| f1bc324eb3 | |||
| f5189f8846 | |||
| 29d24c7a9d | |||
| c360f8fe04 | |||
| 539b7a3a22 | |||
| 8a67eee960 | |||
| de0c275832 | |||
| 97062ef4b8 | |||
| ac5491bf6d | |||
| d49075b4e2 | |||
| e9f3543040 | |||
| 022134a652 | |||
| e551cb0bfe | |||
| 2731decf60 | |||
| 5dad681ab6 | |||
| 96f7c1e37b | |||
| bab787efb1 | |||
| 2601be0777 | |||
| e487b76d1e | |||
| 5788a75acc | |||
| 64f87fe187 | |||
| 84a680d6c3 | |||
| 56d73e0d74 | |||
| b8220a0a46 | |||
| 31bec6b65e | |||
| 0e2313be7b | |||
| a73028796c | |||
| f535c4cff4 | |||
| 5f901d837c | |||
| 3b59616cbe | |||
| 818162b630 | |||
| 991a1b1ab7 | |||
| b94b0926a7 | |||
| f7b11c5166 | |||
| 2e92fccd7b | |||
| 09c60b9543 | |||
| 166476b21c | |||
| 3a775a5b65 | |||
| ab9b6989be | |||
| 1f69cafd16 | |||
| 7030b0bfcc | |||
| 8fb21e8f50 | |||
| f0dd3e4ab8 | |||
| 9719f28795 | |||
| 402ef216c3 | |||
| 5f585e330f | |||
| 6d04874e26 | |||
| 4b45e3f3d2 | |||
| 4359de7cc5 | |||
| 48015a9b1b | |||
| 34df7b1793 | |||
| 429276cac6 | |||
| e7154c93a9 | |||
| 523a9d644b | |||
| 3a6311b7fc | |||
| f6dbca35f3 | |||
| 07e1088839 | |||
| 74d373e37d | |||
| 6ce4d1a7c5 | |||
| 402a3bb2c2 | |||
| 961eaef2a5 | |||
| 6e49a90730 | |||
| 246a9049e6 | |||
| 816f81e5b4 | |||
| 49e32b2600 | |||
| b6993f0b24 | |||
| 58079a8b50 | |||
| bdf080155d | |||
| 52fede6307 | |||
| 7d6c80fa7c | |||
| 814111adb9 | |||
| f0f4191dcf | |||
| 9a80a11577 | |||
| 5551a287a8 | |||
| b0f7c91548 | |||
| 9232e17af4 | |||
| d962137d24 | |||
| 22c91137bb | |||
| 92b8bdd113 | |||
| 3c5178b002 | |||
| 8231005225 | |||
| 2646313c11 | |||
| 12b4875e24 | |||
| 6a98df1c71 | |||
| a5d24f14cb | |||
| 50f914d6d5 | |||
| b4422131fb | |||
| f61b50dbfc | |||
| ce590cced7 | |||
| a417af1ff0 | |||
| d2e650d188 | |||
| 088516b2ed | |||
| 797ea46bb3 | |||
| 96c0a6296f | |||
| e524e030ef | |||
| e19d96f85c | |||
| 0672d8f094 | |||
| 780858f538 | |||
| 7ec495be67 | |||
| d27aef2063 | |||
| d7d07bde75 | |||
| f54ccde5fc | |||
| e1d7c7a081 | |||
| d593384219 | |||
| 5c737d977d | |||
| 22c0af9bb1 | |||
| 1c21b092e6 | |||
| 2325e5f535 | |||
| dbaece9462 | |||
| edd33ab77e | |||
| 38934ddda2 | |||
| 5c9455c32a | |||
| 9e2eca3b8b | |||
| 82a487ec50 | |||
| 466db15ccb | |||
| c7dfb005ba | |||
| 82ae8b64e4 | |||
| 1ffc2e47a1 | |||
| a5c392e176 | |||
| 8f9754dd54 | |||
| b3b3b1d134 | |||
| 26fd70ce4b | |||
| 18aed385f9 | |||
| e6db0ab00d | |||
| 8ed1e6c394 | |||
| da62e614d7 | |||
| ca94909ccc | |||
| 131c1aaf13 | |||
| 5f06b1d43e | |||
| c999de6291 | |||
| a42c8faeca | |||
| ecfbfa2a35 | |||
| 6b86214ca8 | |||
| df6f53ba30 | |||
| 0888f6dba0 | |||
| 5d3c798dfd | |||
| 93d2397f72 | |||
| 05e759a38c | |||
| 67eaa4ac49 | |||
| e8aeebb123 | |||
| d121e97067 | |||
| bdf5c4de8e | |||
| 7492cf8bf5 | |||
| d29d4cba66 | |||
| 655d61e560 | |||
| ba6d210662 | |||
| aff46acdd0 | |||
| 5a16cbe602 | |||
| 519f2d93ba | |||
| 646c091e02 | |||
| 12c3698480 | |||
| c86acd20d6 | |||
| 60b787edc9 | |||
| eec33190d2 | |||
| 6e38759bd1 | |||
| 6a242356a0 | |||
| 04d2ea8773 | |||
| d78ffd7a05 | |||
| 3f21a18f46 | |||
| c4514f94cf | |||
| af39ec1e07 | |||
| c02530759d | |||
| 6df42502ac | |||
| fea13d1d3d | |||
| c7685febcf | |||
| abf5f2b49a | |||
| 24319585b4 | |||
| 812f357d7e | |||
| 51431d9533 | |||
| eb81c70523 | |||
| b52805cd96 | |||
| cb1266225e | |||
| 33ba2ad17c | |||
| 2f2b36f648 | |||
| aa1de750e9 | |||
| 68410f2f37 | |||
| 1f00c9d680 | |||
| a91fd66abc | |||
| 4036375759 | |||
| 9eb668d08a | |||
| ed22b4e09b | |||
| 52090d7b8e | |||
| 3a065c27e8 | |||
| a14f54735c | |||
| c5697c7d68 | |||
| 6381a32685 | |||
| 42230fdd0e | |||
| 4934a83a2d | |||
| 0ef0ff2a78 | |||
| 80d23f1ba1 | |||
| 59b05ece77 | |||
| e467405e60 | |||
| 1f92c3fd70 | |||
| 74e8875280 | |||
| 3c44a013e5 | |||
| 8546542a16 | |||
| f56460cc1f | |||
| 54f25b5b8f | |||
| 541b976b8e | |||
| b6c7e24cad | |||
| 22d624a25d | |||
| d482117fd1 | |||
| 835f00b106 | |||
| 55c3b83fa8 | |||
| 348afa46f0 | |||
| a7890a964f | |||
| ba57ab9a1b | |||
| ac6b396ed3 | |||
| 570bfef4b3 | |||
| 100babc056 | |||
| 5ab8338c3e | |||
| 600b6a7d23 | |||
| f6e465a0ba | |||
| bea006880b | |||
| a7e35f5d78 | |||
| 5fb963abbf | |||
| 351fe62d18 | |||
| 65a713a569 | |||
| 646531cbfc | |||
| 8233ca67bb | |||
| d991069efc | |||
| d86bca9f00 | |||
| 82f71d8e4b | |||
| 315a1baf2c | |||
| e5bd33061b | |||
| 06ed6e6b23 | |||
| 4a578148e2 | |||
| 2c0a029187 | |||
| a3b83afa84 | |||
| 6c2a5b460e | |||
| aa302fe531 | |||
| f2c2e84a74 | |||
| d73a173c6e | |||
| 110b50a3cb | |||
| 36f5afe910 | |||
| 15f79c19a0 | |||
| 7135466092 | |||
| e4c25ed07e | |||
| 273ac9a9c0 | |||
| 7007e71dcf | |||
| 14247854ce | |||
| d863adc8af | |||
| 2e33b1dc56 | |||
| d56ae991c2 | |||
| 2913af90ca | |||
| 553e45c948 | |||
| 2364a08fd6 | |||
| eed5de4045 | |||
| 71a4f88c8b | |||
| db1162e0a1 | |||
| 40a791985e | |||
| 157669b775 | |||
| 3d8d101bfc | |||
| 7e3c8fc8ab | |||
| c58ac19adf | |||
| 35d038cbf7 | |||
| 13a5c54338 | |||
| c0a439fb5f | |||
| 5ce2203786 | |||
| f3931c8587 | |||
| 828abf6326 | |||
| 33ebbab6a8 | |||
| 0a49816b24 | |||
| c77ac9f101 | |||
| 820a13f37a | |||
| 2a29fccd1d | |||
| b857e19392 | |||
| b4bdc66c39 | |||
| c86484b152 | |||
| a33eb51f36 | |||
| 373cd8141c | |||
| 9f00a1d0d2 | |||
| 9a37d7bfbb | |||
| c9c08906e5 |
@@ -61,5 +61,7 @@ updates:
|
|||||||
groups:
|
groups:
|
||||||
actions:
|
actions:
|
||||||
patterns:
|
patterns:
|
||||||
# Combine updates for github provided actions
|
|
||||||
- "actions/*"
|
- "actions/*"
|
||||||
|
docker:
|
||||||
|
patterns:
|
||||||
|
- "docker/*"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: Binance Leverage tiers update
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 3 * * 4"
|
- cron: "25 3 * * 4"
|
||||||
# on demand
|
# on demand
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -24,12 +24,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.14"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
|
||||||
|
with:
|
||||||
|
activate-environment: true
|
||||||
|
enable-cache: false
|
||||||
|
python-version: "3.14"
|
||||||
|
|
||||||
- name: Install ccxt
|
- name: Install ccxt
|
||||||
run: pip install ccxt
|
run: uv pip install $(grep -E "^ccxt==" requirements.txt) $(grep -E "^orjson==" requirements.txt)
|
||||||
|
|
||||||
- name: Run leverage tier update
|
- name: Run leverage tier update
|
||||||
env:
|
env:
|
||||||
@@ -39,7 +46,7 @@ jobs:
|
|||||||
run: python build_helpers/binance_update_lev_tiers.py
|
run: python build_helpers/binance_update_lev_tiers.py
|
||||||
|
|
||||||
|
|
||||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||||
add-paths: freqtrade/exchange/binance_leverage_tiers.json
|
add-paths: freqtrade/exchange/binance_leverage_tiers.json
|
||||||
@@ -48,7 +55,7 @@ jobs:
|
|||||||
Dependencies
|
Dependencies
|
||||||
branch: update/binance-leverage-tiers
|
branch: update/binance-leverage-tiers
|
||||||
title: Update Binance Leverage Tiers
|
title: Update Binance Leverage Tiers
|
||||||
commit-message: "chore: update pre-commit hooks"
|
commit-message: "chore: update binance leverage tiers"
|
||||||
committer: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
committer: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||||
author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||||
body: Update binance leverage tiers.
|
body: Update binance leverage tiers.
|
||||||
|
|||||||
+70
-61
@@ -32,13 +32,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python 🐍
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -55,7 +55,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Installation (python)
|
- name: Installation (python)
|
||||||
run: |
|
run: |
|
||||||
uv pip install --upgrade wheel
|
|
||||||
uv pip install -r requirements-dev.txt
|
uv pip install -r requirements-dev.txt
|
||||||
uv pip install -e ft_client/
|
uv pip install -e ft_client/
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
@@ -78,7 +77,7 @@ jobs:
|
|||||||
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
|
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
|
||||||
|
|
||||||
- name: Cleanup codecov dirty state files
|
- name: Cleanup codecov dirty state files
|
||||||
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
|
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
|
||||||
@@ -137,10 +136,6 @@ jobs:
|
|||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Sort imports (isort)
|
|
||||||
run: |
|
|
||||||
isort --check .
|
|
||||||
|
|
||||||
- name: Run Ruff
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
ruff check --output-format=github
|
ruff check --output-format=github
|
||||||
@@ -161,18 +156,18 @@ jobs:
|
|||||||
$PSVersionTable
|
$PSVersionTable
|
||||||
Get-PSRepository | Format-List *
|
Get-PSRepository | Format-List *
|
||||||
Set-PSRepository psgallery -InstallationPolicy trusted
|
Set-PSRepository psgallery -InstallationPolicy trusted
|
||||||
Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force -SkipPublisherCheck
|
Install-Module -Name Pester -RequiredVersion 5.7.1 -Confirm:$false -Force -SkipPublisherCheck
|
||||||
$Error.clear()
|
$Error.clear()
|
||||||
Invoke-Pester -Path "tests" -CI
|
Invoke-Pester -Path "tests" -CI
|
||||||
if ($Error.Length -gt 0) {exit 1}
|
if ($Error.Length -gt 0) {exit 1}
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
|
||||||
if: ${{ failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }}
|
if: ${{ failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }}
|
||||||
with:
|
with:
|
||||||
severity: error
|
color: '#FF0000' # red
|
||||||
details: Freqtrade CI failed on ${{ matrix.os }} with Python ${{ matrix.python-version }}!
|
title: Freqtrade CI failed on ${{ matrix.os }} with Python ${{ matrix.python-version }}!
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhook: ${{ secrets.DISCORD_WEBHOOK }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
|
||||||
|
|
||||||
mypy-version-check:
|
mypy-version-check:
|
||||||
name: "Mypy Version Check"
|
name: "Mypy Version Check"
|
||||||
@@ -182,14 +177,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python 🐍
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 #v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
|
||||||
|
with:
|
||||||
|
activate-environment: true
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
- name: pre-commit dependencies
|
- name: pre-commit dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install pyaml
|
uv pip install $(grep -E "^pyyaml==" requirements-dev.txt)
|
||||||
python build_helpers/pre_commit_update.py
|
python build_helpers/pre_commit_update.py
|
||||||
|
|
||||||
pre-commit:
|
pre-commit:
|
||||||
@@ -200,9 +201,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.13"
|
||||||
|
|
||||||
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
||||||
|
|
||||||
docs-check:
|
docs-check:
|
||||||
@@ -217,51 +219,59 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
./tests/test_docs.sh
|
./tests/test_docs.sh
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python 🐍
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
|
||||||
|
with:
|
||||||
|
activate-environment: true
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
- name: Documentation build
|
- name: Documentation build
|
||||||
run: |
|
run: |
|
||||||
pip install -r docs/requirements-docs.txt
|
uv pip install -r docs/requirements-docs.txt
|
||||||
mkdocs build
|
mkdocs build
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
|
||||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
severity: error
|
color: '#FF0000' # red
|
||||||
details: Freqtrade doc test failed!
|
title: Freqtrade doc test failed!
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhook: ${{ secrets.DISCORD_WEBHOOK }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
|
||||||
|
|
||||||
|
|
||||||
build-linux-online:
|
build-linux-online:
|
||||||
# Run pytest with "live" checks
|
# Run pytest with "live" checks
|
||||||
name: "Tests and Linting - Online tests"
|
name: "Online / live tests"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.12"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python 🐍
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "${{ matrix.python-version }}"
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: "3.12"
|
python-version: "${{ matrix.python-version }}"
|
||||||
cache-dependency-glob: "requirements**.txt"
|
cache-dependency-glob: "requirements**.txt"
|
||||||
cache-suffix: "3.12"
|
cache-suffix: "3.12"
|
||||||
|
|
||||||
- name: Installation - *nix
|
- name: Installation - *nix
|
||||||
run: |
|
run: |
|
||||||
uv pip install --upgrade wheel
|
|
||||||
uv pip install -r requirements-dev.txt
|
uv pip install -r requirements-dev.txt
|
||||||
uv pip install -e ft_client/
|
uv pip install -e ft_client/
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
@@ -285,22 +295,13 @@ jobs:
|
|||||||
if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade'
|
if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade'
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Check user permission
|
|
||||||
id: check
|
|
||||||
continue-on-error: true
|
|
||||||
uses: prince-chrismc/check-actor-permissions-action@d504e74ba31658f4cdf4fcfeb509d4c09736d88e # v3.0.2
|
|
||||||
with:
|
|
||||||
permission: "write"
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
|
||||||
if: steps.check.outputs.permitted == 'true' && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
|
||||||
with:
|
with:
|
||||||
severity: info
|
color: '#00FF00' # green
|
||||||
details: Test Completed!
|
title: Test Completed!
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhook: ${{ secrets.DISCORD_WEBHOOK }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
|
||||||
|
|
||||||
build:
|
build:
|
||||||
if: always()
|
if: always()
|
||||||
@@ -312,6 +313,9 @@ jobs:
|
|||||||
pre-commit,
|
pre-commit,
|
||||||
]
|
]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.13"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
@@ -324,18 +328,24 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python 🐍
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "${{ matrix.python-version }}"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
|
||||||
|
with:
|
||||||
|
activate-environment: true
|
||||||
|
python-version: "${{ matrix.python-version }}"
|
||||||
|
|
||||||
- name: Build distribution
|
- name: Build distribution
|
||||||
run: |
|
run: |
|
||||||
pip install -U build
|
uv pip install $(grep -E "^build==" requirements-dev.txt)
|
||||||
python -m build --sdist --wheel
|
python -m build --sdist --wheel
|
||||||
|
|
||||||
- name: Upload artifacts 📦
|
- name: Upload artifacts 📦
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.1.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: freqtrade-build
|
name: freqtrade-build
|
||||||
path: |
|
path: |
|
||||||
@@ -344,11 +354,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Client distribution
|
- name: Build Client distribution
|
||||||
run: |
|
run: |
|
||||||
pip install -U build
|
|
||||||
python -m build --sdist --wheel ft_client
|
python -m build --sdist --wheel ft_client
|
||||||
|
|
||||||
- name: Upload artifacts 📦
|
- name: Upload artifacts 📦
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.1.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: freqtrade-client-build
|
name: freqtrade-client-build
|
||||||
path: |
|
path: |
|
||||||
@@ -372,7 +381,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download artifact 📦
|
- name: Download artifact 📦
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
pattern: freqtrade*-build
|
pattern: freqtrade*-build
|
||||||
path: dist
|
path: dist
|
||||||
@@ -401,7 +410,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download artifact 📦
|
- name: Download artifact 📦
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
pattern: freqtrade*-build
|
pattern: freqtrade*-build
|
||||||
path: dist
|
path: dist
|
||||||
@@ -422,9 +431,9 @@ jobs:
|
|||||||
packages: write # Needed to push package versions
|
packages: write # Needed to push package versions
|
||||||
contents: read
|
contents: read
|
||||||
secrets:
|
secrets:
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
packages-cleanup:
|
packages-cleanup:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ name: Docker Build and Deploy
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
secrets:
|
secrets:
|
||||||
DOCKER_PASSWORD:
|
DOCKERHUB_USERNAME:
|
||||||
required: true
|
required: true
|
||||||
DOCKER_USERNAME:
|
DOCKERHUB_TOKEN:
|
||||||
required: true
|
required: true
|
||||||
DISCORD_WEBHOOK:
|
DISCORD_WEBHOOK:
|
||||||
required: false
|
required: false
|
||||||
@@ -35,6 +35,8 @@ jobs:
|
|||||||
name: "Deploy Docker x64 and armv7l"
|
name: "Deploy Docker x64 and armv7l"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
if: github.repository == 'freqtrade/freqtrade'
|
if: github.repository == 'freqtrade/freqtrade'
|
||||||
|
environment:
|
||||||
|
name: docker
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
@@ -57,19 +59,19 @@ jobs:
|
|||||||
uses: ./.github/actions/docker-tags
|
uses: ./.github/actions/docker-tags
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
with:
|
with:
|
||||||
cache-image: false
|
cache-image: false
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f #v3.12.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd #v4.0.0
|
||||||
|
|
||||||
- name: Available platforms
|
- name: Available platforms
|
||||||
run: echo ${PLATFORMS}
|
run: echo ${PLATFORMS}
|
||||||
@@ -168,6 +170,8 @@ jobs:
|
|||||||
# Only run on 64bit machines
|
# Only run on 64bit machines
|
||||||
runs-on: [self-hosted, linux, ARM64]
|
runs-on: [self-hosted, linux, ARM64]
|
||||||
if: github.repository == 'freqtrade/freqtrade'
|
if: github.repository == 'freqtrade/freqtrade'
|
||||||
|
environment:
|
||||||
|
name: docker
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
@@ -179,13 +183,13 @@ jobs:
|
|||||||
uses: ./.github/actions/docker-tags
|
uses: ./.github/actions/docker-tags
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to github
|
- name: Login to github
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -306,9 +310,8 @@ jobs:
|
|||||||
docker image prune -a --force --filter "until=24h"
|
docker image prune -a --force --filter "until=24h"
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
|
||||||
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && (github.event_name != 'schedule')
|
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && (github.event_name != 'schedule')
|
||||||
with:
|
with:
|
||||||
severity: info
|
title: Deploy Succeeded!
|
||||||
details: Deploy Succeeded!
|
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- stable
|
- stable
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}
|
group: ${{ github.workflow }}
|
||||||
@@ -15,6 +16,8 @@ jobs:
|
|||||||
dockerHubDescription:
|
dockerHubDescription:
|
||||||
name: "Update Docker Hub Description"
|
name: "Update Docker Hub Description"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: docker
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
@@ -23,6 +26,6 @@ jobs:
|
|||||||
- name: Docker Hub Description
|
- name: Docker Hub Description
|
||||||
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
|
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
repository: freqtradeorg/freqtrade
|
repository: freqtradeorg/freqtrade
|
||||||
|
|||||||
@@ -17,23 +17,31 @@ jobs:
|
|||||||
auto-update:
|
auto-update:
|
||||||
name: Auto-update pre-commit hooks
|
name: Auto-update pre-commit hooks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: develop
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
|
||||||
|
with:
|
||||||
|
activate-environment: true
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
- name: Install pre-commit
|
- name: Install pre-commit
|
||||||
run: pip install pre-commit
|
run: uv pip install $(grep -E "^pre-commit==" requirements-dev.txt)
|
||||||
|
|
||||||
- name: Run auto-update
|
- name: Run auto-update
|
||||||
run: pre-commit autoupdate
|
run: pre-commit autoupdate
|
||||||
|
|
||||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||||
add-paths: .pre-commit-config.yaml
|
add-paths: .pre-commit-config.yaml
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run zizmor 🌈
|
- name: Run zizmor 🌈
|
||||||
uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1
|
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||||
|
|||||||
+7
-20
@@ -13,12 +13,6 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
additional_dependencies: ["python-rapidjson", "jsonschema"]
|
additional_dependencies: ["python-rapidjson", "jsonschema"]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/flake8
|
|
||||||
rev: "7.3.0"
|
|
||||||
hooks:
|
|
||||||
- id: flake8
|
|
||||||
additional_dependencies: [Flake8-pyproject]
|
|
||||||
# stages: [push]
|
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v1.19.1"
|
rev: "v1.19.1"
|
||||||
@@ -29,22 +23,15 @@ repos:
|
|||||||
- types-cachetools==6.2.0.20251022
|
- types-cachetools==6.2.0.20251022
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.32.4.20260107
|
- types-requests==2.32.4.20260107
|
||||||
- types-tabulate==0.9.0.20241207
|
- types-tabulate==0.10.0.20260308
|
||||||
- types-python-dateutil==2.9.0.20251115
|
- types-python-dateutil==2.9.0.20260305
|
||||||
- scipy-stubs==1.17.0.1
|
- scipy-stubs==1.17.1.2
|
||||||
- SQLAlchemy==2.0.45
|
- SQLAlchemy==2.0.48
|
||||||
# stages: [push]
|
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
|
||||||
rev: "7.0.0"
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
name: isort (python)
|
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.14.14'
|
rev: 'v0.15.7'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
@@ -75,7 +62,7 @@ repos:
|
|||||||
- id: strip-exif
|
- id: strip-exif
|
||||||
|
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.4.1
|
rev: v2.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
@@ -83,6 +70,6 @@ repos:
|
|||||||
|
|
||||||
# Ensure github actions remain safe
|
# Ensure github actions remain safe
|
||||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||||
rev: v1.22.0
|
rev: v1.23.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: zizmor
|
- id: zizmor
|
||||||
|
|||||||
+3
-3
@@ -12,7 +12,8 @@ Few pointers for contributions:
|
|||||||
- Stick to english in both commit messages, PR descriptions and code comments and variable names.
|
- Stick to english in both commit messages, PR descriptions and code comments and variable names.
|
||||||
- New features need to contain unit tests, must pass CI (run pre-commit and pytest to get an early feedback) and should be documented with the introduction PR.
|
- New features need to contain unit tests, must pass CI (run pre-commit and pytest to get an early feedback) and should be documented with the introduction PR.
|
||||||
- PR's can be declared as draft - signaling Work in Progress for Pull Requests (which are not finished). We'll still aim to provide feedback on draft PR's in a timely manner.
|
- PR's can be declared as draft - signaling Work in Progress for Pull Requests (which are not finished). We'll still aim to provide feedback on draft PR's in a timely manner.
|
||||||
- If you're using AI for your PR, please both mention it in the PR description and do a thorough review of the generated code. The final responsibility for the code with the PR author, not with the AI.
|
- If you're using AI for your PR, please both mention it in the PR description and do a thorough review of the generated code yourself.
|
||||||
|
The final responsibility for the code with the PR author, not with the AI, which also means that commits must be linked to your (human) account, not some generic AI account.
|
||||||
|
|
||||||
If you are unsure, discuss the feature on our [discord server](https://discord.gg/p7nuUNVfP7) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a Pull Request.
|
If you are unsure, discuss the feature on our [discord server](https://discord.gg/p7nuUNVfP7) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a Pull Request.
|
||||||
|
|
||||||
@@ -24,8 +25,7 @@ Best start by reading the [documentation](https://www.freqtrade.io/) to get a fe
|
|||||||
|
|
||||||
### 1. Run unit tests
|
### 1. Run unit tests
|
||||||
|
|
||||||
All unit tests must pass. If a unit test is broken, change your code to
|
All unit tests must pass. If a unit test is broken, change your code to make it pass. It means you have introduced a regression.
|
||||||
make it pass. It means you have introduced a regression.
|
|
||||||
|
|
||||||
#### Test the whole project
|
#### Test the whole project
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.13.11-slim-trixie AS base
|
FROM python:3.13.12-slim-trixie AS base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
[](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
|
[](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
|
||||||
[](https://doi.org/10.21105/joss.04864)
|
[](https://doi.org/10.21105/joss.04864)
|
||||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[](https://codecov.io/gh/freqtrade/freqtrade)
|
||||||
[](https://www.freqtrade.io)
|
[](https://www.freqtrade.io)
|
||||||
|
[](https://discord.gg/p7nuUNVfP7)
|
||||||
|
|
||||||
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ hesitate to read the source code and understand the mechanism of this bot.
|
|||||||
|
|
||||||
## Supported Exchange marketplaces
|
## Supported Exchange marketplaces
|
||||||
|
|
||||||
Please read the [exchange-specific notes](docs/exchanges.md) to learn about special configurations that maybe needed for each exchange.
|
Please read the [exchange-specific notes](https://www.freqtrade.io/en/stable/exchanges/) to learn about special configurations that maybe needed for each exchange.
|
||||||
|
|
||||||
### Supported Spot Exchanges
|
### Supported Spot Exchanges
|
||||||
|
|
||||||
@@ -49,8 +50,9 @@ Please read the [exchange-specific notes](docs/exchanges.md) to learn about spec
|
|||||||
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
|
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
|
||||||
- [X] [OKX](https://okx.com/)
|
- [X] [OKX](https://okx.com/)
|
||||||
- [X] [Bybit](https://bybit.com/)
|
- [X] [Bybit](https://bybit.com/)
|
||||||
|
- [X] [Kraken](https://www.kraken.com/features/futures)
|
||||||
|
|
||||||
Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in.
|
Please make sure to read the [exchange specific notes](https://www.freqtrade.io/en/stable/exchanges/), as well as the [trading with leverage](https://www.freqtrade.io/en/stable/leverage/) documentation before diving in.
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
|
|
||||||
@@ -142,7 +144,7 @@ options:
|
|||||||
|
|
||||||
### Telegram RPC commands
|
### Telegram RPC commands
|
||||||
|
|
||||||
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on the [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
|
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on the [documentation](https://www.freqtrade.io/en/stable/telegram-usage/)
|
||||||
|
|
||||||
- `/start`: Starts the trader.
|
- `/start`: Starts the trader.
|
||||||
- `/stop`: Stops the trader.
|
- `/stop`: Stops the trader.
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -649,6 +649,7 @@
|
|||||||
"ProducerPairList",
|
"ProducerPairList",
|
||||||
"RemotePairList",
|
"RemotePairList",
|
||||||
"MarketCapPairList",
|
"MarketCapPairList",
|
||||||
|
"CrossMarketPairList",
|
||||||
"AgeFilter",
|
"AgeFilter",
|
||||||
"DelistFilter",
|
"DelistFilter",
|
||||||
"FullTradesFilter",
|
"FullTradesFilter",
|
||||||
@@ -1057,7 +1058,8 @@
|
|||||||
},
|
},
|
||||||
"jwt_secret_key": {
|
"jwt_secret_key": {
|
||||||
"description": "Secret key for JWT authentication.",
|
"description": "Secret key for JWT authentication.",
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"default": "somethingRandomSomethingRandom123"
|
||||||
},
|
},
|
||||||
"CORS_origins": {
|
"CORS_origins": {
|
||||||
"description": "List of allowed CORS origins.",
|
"description": "List of allowed CORS origins.",
|
||||||
@@ -1080,7 +1082,8 @@
|
|||||||
"listen_ip_address",
|
"listen_ip_address",
|
||||||
"listen_port",
|
"listen_port",
|
||||||
"username",
|
"username",
|
||||||
"password"
|
"password",
|
||||||
|
"jwt_secret_key"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"db_url": {
|
"db_url": {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
"listen_ip_address": "127.0.0.1",
|
"listen_ip_address": "127.0.0.1",
|
||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
"verbosity": "error",
|
"verbosity": "error",
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": [],
|
"CORS_origins": [],
|
||||||
"username": "freqtrader",
|
"username": "freqtrader",
|
||||||
"password": "SuperSecurePassword"
|
"password": "SuperSecurePassword"
|
||||||
|
|||||||
@@ -177,7 +177,7 @@
|
|||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
"verbosity": "error",
|
"verbosity": "error",
|
||||||
"enable_openapi": false,
|
"enable_openapi": false,
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": [],
|
"CORS_origins": [],
|
||||||
"username": "freqtrader",
|
"username": "freqtrader",
|
||||||
"password": "SuperSecurePassword",
|
"password": "SuperSecurePassword",
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
"listen_ip_address": "127.0.0.1",
|
"listen_ip_address": "127.0.0.1",
|
||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
"verbosity": "error",
|
"verbosity": "error",
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": [],
|
"CORS_origins": [],
|
||||||
"username": "freqtrader",
|
"username": "freqtrader",
|
||||||
"password": "SuperSecurePassword"
|
"password": "SuperSecurePassword"
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- "./user_data:/freqtrade/user_data"
|
- "./user_data:/freqtrade/user_data"
|
||||||
# Expose api on port 8080 (localhost only)
|
# Expose api on port 8080 (localhost only)
|
||||||
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
|
||||||
# before enabling this.
|
# before enabling this.
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8080:8080"
|
- "127.0.0.1:8080:8080"
|
||||||
@@ -100,7 +100,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- "./user_data:/freqtrade/user_data"
|
- "./user_data:/freqtrade/user_data"
|
||||||
# Expose api on port 8080 (localhost only)
|
# Expose api on port 8080 (localhost only)
|
||||||
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
|
||||||
# before enabling this.
|
# before enabling this.
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8081:8080"
|
- "127.0.0.1:8081:8080"
|
||||||
|
|||||||
@@ -64,18 +64,15 @@ options:
|
|||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a space-separated list of strategies to
|
Provide a space-separated list of strategies to
|
||||||
backtest. Please note that timeframe needs to be set
|
backtest. Please note that timeframe needs to be set
|
||||||
either in config or via command line. When using this
|
either in config or via command line.
|
||||||
together with `--export trades`, the strategy-name is
|
|
||||||
injected into the filename (so `backtest-data.json`
|
|
||||||
becomes `backtest-data-SampleStrategy.json`
|
|
||||||
--export {none,trades,signals}
|
--export {none,trades,signals}
|
||||||
Export backtest results (default: trades).
|
Export backtest results (default: trades).
|
||||||
--backtest-filename, --export-filename PATH
|
--backtest-filename, --export-filename PATH
|
||||||
Use this filename for backtest results.Example:
|
DEPRECATED: This option is deprecated for backtesting
|
||||||
`--backtest-
|
and will be removed in a future release. Using a
|
||||||
filename=backtest_results_2020-09-27_16-20-48.json`.
|
custom filename for backtest results is no longer
|
||||||
Assumes either `user_data/backtest_results/` or
|
supported. Use `--backtest-directory` to specify the
|
||||||
`--export-directory` as base directory.
|
directory.
|
||||||
--backtest-directory, --export-directory PATH
|
--backtest-directory, --export-directory PATH
|
||||||
Directory to use for backtest results. Example:
|
Directory to use for backtest results. Example:
|
||||||
`--export-directory=user_data/backtest_results/`.
|
`--export-directory=user_data/backtest_results/`.
|
||||||
|
|||||||
@@ -62,10 +62,7 @@ options:
|
|||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a space-separated list of strategies to
|
Provide a space-separated list of strategies to
|
||||||
backtest. Please note that timeframe needs to be set
|
backtest. Please note that timeframe needs to be set
|
||||||
either in config or via command line. When using this
|
either in config or via command line.
|
||||||
together with `--export trades`, the strategy-name is
|
|
||||||
injected into the filename (so `backtest-data.json`
|
|
||||||
becomes `backtest-data-SampleStrategy.json`
|
|
||||||
--export {none,trades,signals}
|
--export {none,trades,signals}
|
||||||
Export backtest results (default: trades).
|
Export backtest results (default: trades).
|
||||||
--backtest-filename, --export-filename PATH
|
--backtest-filename, --export-filename PATH
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ options:
|
|||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a space-separated list of strategies to
|
Provide a space-separated list of strategies to
|
||||||
backtest. Please note that timeframe needs to be set
|
backtest. Please note that timeframe needs to be set
|
||||||
either in config or via command line. When using this
|
either in config or via command line.
|
||||||
together with `--export trades`, the strategy-name is
|
|
||||||
injected into the filename (so `backtest-data.json`
|
|
||||||
becomes `backtest-data-SampleStrategy.json`
|
|
||||||
--strategy-path PATH Specify additional strategy lookup path.
|
--strategy-path PATH Specify additional strategy lookup path.
|
||||||
--recursive-strategy-search
|
--recursive-strategy-search
|
||||||
Recursively search for a strategy in the strategies
|
Recursively search for a strategy in the strategies
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| | **Unfilled timeout**
|
| | **Unfilled timeout**
|
||||||
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `"minutes"`.* <br> **Datatype:** String
|
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set `unfilledtimeout.unit` to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `"minutes"`.* <br> **Datatype:** String
|
||||||
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
|
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
|
||||||
| | **Pricing**
|
| | **Pricing**
|
||||||
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).<br> *Defaults to `"same"`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).<br> *Defaults to `"same"`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
||||||
@@ -229,7 +229,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `exchange.enable_ws` | Enable the usage of Websockets for the exchange. <br>[More information](#consuming-exchange-websockets).<br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `exchange.enable_ws` | Enable the usage of Websockets for the exchange. <br>[More information](#consuming-exchange-websockets).<br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||||
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||||
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||||
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
|
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`*<br> **Datatype:** float
|
||||||
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||||
| `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
|
| `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
|
| `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
|
||||||
@@ -240,7 +240,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
|
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
|
||||||
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `true`.<br> **Datatype:** boolean
|
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `true`.*<br> **Datatype:** boolean
|
||||||
| `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.<br> **Datatype:** dictionary
|
| `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.<br> **Datatype:** dictionary
|
||||||
| `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function. <br> **Datatype:** Boolean
|
| `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function. <br> **Datatype:** Boolean
|
||||||
| | **Webhook**
|
| | **Webhook**
|
||||||
@@ -280,8 +280,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `add_config_files` | Additional config files. These files will be loaded and merged with the current config file. The files are resolved relative to the initial file.<br> *Defaults to `[]`*. <br> **Datatype:** List of strings
|
| `add_config_files` | Additional config files. These files will be loaded and merged with the current config file. The files are resolved relative to the initial file.<br> *Defaults to `[]`*. <br> **Datatype:** List of strings
|
||||||
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
||||||
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
||||||
| `reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage (and decreasing train/inference timing backtesting/hyperopt and in FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage (and decreasing train/inference timing backtesting/hyperopt and in FreqAI). <br> Default: `False`. <br> **Datatype:** Boolean.
|
||||||
| `log_config` | Dictionary containing the log config for python logging. [more info](advanced-setup.md#advanced-logging) <br> **Datatype:** dict. <br> Default: `FtRichHandler`
|
| `log_config` | Dictionary containing the log config for python logging. [more info](advanced-setup.md#advanced-logging) <br> Default: `FtRichHandler` <br> **Datatype:** dict.
|
||||||
|
|
||||||
### Parameters in the strategy
|
### Parameters in the strategy
|
||||||
|
|
||||||
|
|||||||
@@ -269,6 +269,8 @@ If `--convert` is also provided, the resample step will happen automatically and
|
|||||||
!!! Note "Kraken user"
|
!!! Note "Kraken user"
|
||||||
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
|
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
|
||||||
|
|
||||||
|
Kraken Futures uses standard OHLCV downloads and does not require `--dl-trades`.
|
||||||
|
|
||||||
Example call:
|
Example call:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+34
-1
@@ -217,6 +217,32 @@ freqtrade download-data --exchange kraken --dl-trades -p BTC/EUR BCH/EUR
|
|||||||
Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests/sec rate.
|
Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests/sec rate.
|
||||||
So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased.
|
So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased.
|
||||||
|
|
||||||
|
## Kraken Futures
|
||||||
|
|
||||||
|
Kraken Futures uses the exchange id `krakenfutures` and supports isolated futures mode.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"exchange": {
|
||||||
|
"name": "krakenfutures",
|
||||||
|
"key": "your_exchange_key",
|
||||||
|
"secret": "your_exchange_secret"
|
||||||
|
},
|
||||||
|
"trading_mode": "futures",
|
||||||
|
"margin_mode": "isolated",
|
||||||
|
"stake_currency": "USD"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Kraken Futures supports `stoploss_on_exchange` with both `limit` and `market` stop orders.
|
||||||
|
Use `order_types.stoploss_price_type` to select the trigger price source (`mark`, `last`, or `index`).
|
||||||
|
|
||||||
|
!!! Note "Collateral"
|
||||||
|
Kraken Futures is USD-settled. Use USD as your stake currency.
|
||||||
|
|
||||||
|
!!! Note "Flex (Multi-collateral) Accounts"
|
||||||
|
Kraken Futures flex accounts allow collateral in multiple currencies, while trading remains USD-settled.
|
||||||
|
Freqtrade derives the `USD` balance from Kraken margin fields, so keep `stake_currency` set to `USD`.
|
||||||
|
|
||||||
## Kucoin
|
## Kucoin
|
||||||
|
|
||||||
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||||
@@ -319,7 +345,6 @@ API Keys for live futures trading must have the following permissions:
|
|||||||
|
|
||||||
We do strongly recommend to limit all API keys to the IP you're going to use it from.
|
We do strongly recommend to limit all API keys to the IP you're going to use it from.
|
||||||
|
|
||||||
|
|
||||||
## Bitmart
|
## Bitmart
|
||||||
|
|
||||||
Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret.
|
Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret.
|
||||||
@@ -369,6 +394,11 @@ On startup, freqtrade will set the position mode to "One-way Mode" for the whole
|
|||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Hyperliquid supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it.
|
Hyperliquid supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
|
|
||||||
|
!!! Warning "Unified accounts"
|
||||||
|
Hyperliquid unified accounts are supported - though this relies freqtrade's assumption of "owning" the account, and being the only one trading on it (in this case, extended to both spot and futures).
|
||||||
|
We hence recommend the usage of subaccounts where possible, and to avoid manual trading on the same account while the bot is running.
|
||||||
|
Freqtrade will attempt to detect the account type on startup - changing the account type mid-trading is not supported and may lead to exceptions and errors.
|
||||||
|
|
||||||
Hyperliquid is a Decentralized Exchange (DEX). Decentralized exchanges work a bit different compared to normal exchanges. Instead of authenticating private API calls using an API key, private API calls need to be signed with the private key of your wallet (We recommend using an api Wallet for this, generated either on Hyperliquid or in your wallet of choice).
|
Hyperliquid is a Decentralized Exchange (DEX). Decentralized exchanges work a bit different compared to normal exchanges. Instead of authenticating private API calls using an API key, private API calls need to be signed with the private key of your wallet (We recommend using an api Wallet for this, generated either on Hyperliquid or in your wallet of choice).
|
||||||
This needs to be configured like this:
|
This needs to be configured like this:
|
||||||
|
|
||||||
@@ -424,6 +454,7 @@ Your balance and trades will now be used from your vault / subaccount - and no l
|
|||||||
!!! Note
|
!!! Note
|
||||||
You can only use either a vault or a subaccount - not both at the same time.
|
You can only use either a vault or a subaccount - not both at the same time.
|
||||||
|
|
||||||
|
|
||||||
### Historic Hyperliquid data
|
### Historic Hyperliquid data
|
||||||
|
|
||||||
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
|
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
|
||||||
@@ -458,6 +489,8 @@ Replace `"dex_name_1"` and `"dex_name_2"` with the actual names of the HIP-3 DEX
|
|||||||
!!! Note
|
!!! Note
|
||||||
HIP-3 DEXes share the same wallet and free amount of collateral as your main Hyperliquid account. Trades on different DEXes will affect your overall account balance and margin.
|
HIP-3 DEXes share the same wallet and free amount of collateral as your main Hyperliquid account. Trades on different DEXes will affect your overall account balance and margin.
|
||||||
|
|
||||||
|
The pair name for HIP-3 pairs will be slightly different than non HIP-3 pairs. Please use `list-pairs` subcommand to get the correct pair naming for all pairs for the specified dexes.
|
||||||
|
|
||||||
## Bitvavo
|
## Bitvavo
|
||||||
|
|
||||||
If your account is required to use an operatorId, you can set it in the configuration file as follows:
|
If your account is required to use an operatorId, you can set it in the configuration file as follows:
|
||||||
|
|||||||
@@ -260,6 +260,10 @@ freqtrade trade --config config_examples/config_freqai.example.json --strategy F
|
|||||||
|
|
||||||
PyTorch dropped support for macOS x64 (intel based Apple devices) in version 2.3. Subsequently, freqtrade also dropped support for PyTorch on this platform.
|
PyTorch dropped support for macOS x64 (intel based Apple devices) in version 2.3. Subsequently, freqtrade also dropped support for PyTorch on this platform.
|
||||||
|
|
||||||
|
!!! Danger "Security notice"
|
||||||
|
Loading saved models from disk can cause security issues if using remote model files (files you downloaded from the internet or received from an untrusted source) due to having the necessity to have `weights_only=False`, which can cause security problems.
|
||||||
|
As long as you only load models that you have trained yourself, there is no risk.
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
#### Model
|
#### Model
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `n_epochs` | The `n_epochs` parameter is a crucial setting in the PyTorch training loop that determines the number of times the entire training dataset will be used to update the model's parameters. An epoch represents one full pass through the entire training dataset. Overrides `n_steps`. Either `n_epochs` or `n_steps` must be set. <br><br> **Datatype:** int. optional. <br> Default: `10`.
|
| `n_epochs` | The `n_epochs` parameter is a crucial setting in the PyTorch training loop that determines the number of times the entire training dataset will be used to update the model's parameters. An epoch represents one full pass through the entire training dataset. Overrides `n_steps`. Either `n_epochs` or `n_steps` must be set. <br><br> **Datatype:** int. optional. <br> Default: `10`.
|
||||||
| `n_steps` | An alternative way of setting `n_epochs` - the number of training iterations to run. Iteration here refer to the number of times we call `optimizer.step()`. Ignored if `n_epochs` is set. A simplified version of the function: <br><br> n_epochs = n_steps / (n_obs / batch_size) <br><br> The motivation here is that `n_steps` is easier to optimize and keep stable across different n_obs - the number of data points. <br> <br> **Datatype:** int. optional. <br> Default: `None`.
|
| `n_steps` | An alternative way of setting `n_epochs` - the number of training iterations to run. Iteration here refer to the number of times we call `optimizer.step()`. Ignored if `n_epochs` is set. A simplified version of the function: <br><br> n_epochs = n_steps / (n_obs / batch_size) <br><br> The motivation here is that `n_steps` is easier to optimize and keep stable across different n_obs - the number of data points. <br> <br> **Datatype:** int. optional. <br> Default: `None`.
|
||||||
| `batch_size` | The size of the batches to use during training. <br><br> **Datatype:** int. <br> Default: `64`.
|
| `batch_size` | The size of the batches to use during training. <br><br> **Datatype:** int. <br> Default: `64`.
|
||||||
|
| `early_stopping_patience` | Number of epochs with no improvement in validation loss before training is stopped early. This helps prevent overfitting by halting training when the model stops improving. Set to `0` to disable early stopping. Requires a test/validation split (`test_size > 0`). <br><br> **Datatype:** int. <br> Default: `0` (disabled).
|
||||||
|
|
||||||
### Additional parameters
|
### Additional parameters
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from
|
|||||||
|
|
||||||
More details about feature engineering available:
|
More details about feature engineering available:
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
https://www.freqtrade.io/en/stable/freqai-feature-engineering
|
||||||
|
|
||||||
:param df: strategy dataframe which will receive the targets
|
:param df: strategy dataframe which will receive the targets
|
||||||
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ To save the models generated during a particular backtest so that you can start
|
|||||||
To ensure that the model can be reused, freqAI will call your strategy with a dataframe of length 1.
|
To ensure that the model can be reused, freqAI will call your strategy with a dataframe of length 1.
|
||||||
If your strategy requires more data than this to generate the same features, you can't reuse backtest predictions for live deployment and need to update your `identifier` for each new backtest.
|
If your strategy requires more data than this to generate the same features, you can't reuse backtest predictions for live deployment and need to update your `identifier` for each new backtest.
|
||||||
|
|
||||||
|
!!! Danger "Security notice"
|
||||||
|
Loading saved models from disk can cause security issues if using remote model files (files you downloaded from the internet or received from an untrusted source) due to having the necessity to have `weights_only=False`, which can cause security problems.
|
||||||
|
As long as you only load models that you have trained yourself, there is no risk.
|
||||||
|
|
||||||
### Backtest live collected predictions
|
### Backtest live collected predictions
|
||||||
|
|
||||||
FreqAI allow you to reuse live historic predictions through the backtest parameter `--freqai-backtest-live-models`. This can be useful when you want to reuse predictions generated in dry/run for comparison or other study.
|
FreqAI allow you to reuse live historic predictions through the backtest parameter `--freqai-backtest-live-models`. This can be useful when you want to reuse predictions generated in dry/run for comparison or other study.
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, FreqAI aims to be a sandbox for easily deploying robust machine learning libraries on real-time data ([details](#freqai-position-in-open-source-machine-learning-landscape)).
|
FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, FreqAI aims to be a sandbox for easily deploying robust machine learning libraries on real-time data ([details](#freqai-position-in-open-source-machine-learning-landscape)).
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
FreqAI is, and always will be, a not-for-profit, open source project. FreqAI does *not* have a crypto token, FreqAI does *not* sell signals, and FreqAI does not have a domain besides the present [freqtrade documentation](https://www.freqtrade.io/en/latest/freqai/).
|
FreqAI is, and always will be, a not-for-profit, open source project. FreqAI does *not* have a crypto token, FreqAI does *not* sell signals, and FreqAI does not have a domain besides the present [freqtrade documentation](https://www.freqtrade.io/en/stable/freqai/).
|
||||||
|
|
||||||
Features include:
|
Features include:
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - t
|
|||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
//...
|
//...
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": ["https://frequi.freqtrade.io"],
|
"CORS_origins": ["https://frequi.freqtrade.io"],
|
||||||
//...
|
//...
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ The correct configuration for this case is `http://localhost:8080` - the main pa
|
|||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
//...
|
//...
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": ["http://localhost:8080"],
|
"CORS_origins": ["http://localhost:8080"],
|
||||||
//...
|
//...
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
| [Hyperliquid](exchanges.md#hyperliquid) | spot | | ❌ (not supported) |
|
| [Hyperliquid](exchanges.md#hyperliquid) | spot | | ❌ (not supported) |
|
||||||
| [Hyperliquid](exchanges.md#hyperliquid) | futures | isolated, cross | limit |
|
| [Hyperliquid](exchanges.md#hyperliquid) | futures | isolated, cross | limit |
|
||||||
| [Kraken](exchanges.md#kraken) | spot | | market, limit |
|
| [Kraken](exchanges.md#kraken) | spot | | market, limit |
|
||||||
|
| [Kraken](exchanges.md#kraken-futures) | futures | isolated | market, limit |
|
||||||
| [OKX](exchanges.md#okx) | spot | | limit |
|
| [OKX](exchanges.md#okx) | spot | | limit |
|
||||||
| [OKX](exchanges.md#okx) | futures | isolated | limit |
|
| [OKX](exchanges.md#okx) | futures | isolated | limit |
|
||||||
| [Bitvavo](exchanges.md#bitvavo) | spot | | ❌ (not supported) |
|
| [Bitvavo](exchanges.md#bitvavo) | spot | | ❌ (not supported) |
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
|
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
|
||||||
|
|
||||||
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) and [`PercentChangePairList`](#percent-change-pair-list) Pairlist Handlers).
|
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list), [`CrossMarketPairList`](#crossmarketpairlist), [`MarketCapPairlist`](#marketcappairlist) and [`PercentChangePairList`](#percent-change-pair-list) Pairlist Handlers).
|
||||||
|
|
||||||
Additionally, [`AgeFilter`](#agefilter), [`DelistFilter`](#delistfilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
|
Additionally, [`AgeFilter`](#agefilter), [`DelistFilter`](#delistfilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
|
||||||
|
|
||||||
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList`, `MarketCapPairList` or `PercentChangePairList` as the starting Pairlist Handler.
|
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList`, `MarketCapPairList`, `PercentChangePairList` or `CrossMarketPairList` as the starting Pairlist Handler.
|
||||||
|
|
||||||
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
|
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
|
|||||||
* [`ProducerPairList`](#producerpairlist)
|
* [`ProducerPairList`](#producerpairlist)
|
||||||
* [`RemotePairList`](#remotepairlist)
|
* [`RemotePairList`](#remotepairlist)
|
||||||
* [`MarketCapPairList`](#marketcappairlist)
|
* [`MarketCapPairList`](#marketcappairlist)
|
||||||
|
* [`CrossMarketPairList`](#crossmarketpairlist)
|
||||||
* [`AgeFilter`](#agefilter)
|
* [`AgeFilter`](#agefilter)
|
||||||
* [`DelistFilter`](#delistfilter)
|
* [`DelistFilter`](#delistfilter)
|
||||||
* [`FullTradesFilter`](#fulltradesfilter)
|
* [`FullTradesFilter`](#fulltradesfilter)
|
||||||
@@ -303,6 +304,8 @@ The optional `mode` option specifies if the pairlist should be used as a `blackl
|
|||||||
|
|
||||||
The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append". The default value is "filter".
|
The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append". The default value is "filter".
|
||||||
|
|
||||||
|
The optional `number_assets` option in the RemotePairList configuration determines how many pairs will be returned if used in whitelist `mode`. By default, all pairs will be returned. In blacklist `mode`, this option will be ignored.
|
||||||
|
|
||||||
In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out.
|
In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out.
|
||||||
|
|
||||||
In "append" mode, the retrieved pairlist is added to the original pairlist. All pairs from both lists are included in the final pairlist without any filtering.
|
In "append" mode, the retrieved pairlist is added to the original pairlist. All pairs from both lists are included in the final pairlist without any filtering.
|
||||||
@@ -402,6 +405,12 @@ Coins like 1000PEPE/USDT or KPEPE/USDT:USDT are detected on a best effort basis,
|
|||||||
!!! Danger "Duplicate symbols in coingecko"
|
!!! Danger "Duplicate symbols in coingecko"
|
||||||
Coingecko often has duplicate symbols, where the same symbol is used for different coins. Freqtrade will use the symbol as is and try to search for it on the exchange. If the symbol exists - it will be used. Freqtrade will however not check if the _intended_ symbol is the one coingecko meant. This can sometimes lead to unexpected results, especially on low volume coins or with meme coin categories.
|
Coingecko often has duplicate symbols, where the same symbol is used for different coins. Freqtrade will use the symbol as is and try to search for it on the exchange. If the symbol exists - it will be used. Freqtrade will however not check if the _intended_ symbol is the one coingecko meant. This can sometimes lead to unexpected results, especially on low volume coins or with meme coin categories.
|
||||||
|
|
||||||
|
#### CrossMarketPairList
|
||||||
|
|
||||||
|
Generate or filter pairs based of their availability on the opposite market.
|
||||||
|
|
||||||
|
The `pairs_exist_on` setting defines whether the pairs should exists on both spot and futures market (`both_markets`) or only exist on the specified trading mode (`current_market_only`). By default, the plugin will be in `both_markets` setting, which means whitelisted pairs have to exists on both spot and futures markets.
|
||||||
|
|
||||||
#### AgeFilter
|
#### AgeFilter
|
||||||
|
|
||||||
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).
|
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
|
|||||||
|
|
||||||
### Common settings to all Protections
|
### Common settings to all Protections
|
||||||
|
|
||||||
| Parameter| Description |
|
| Parameter | Description |
|
||||||
|------------|-------------|
|
| --------- | ---------- |
|
||||||
| `method` | Protection name to use. <br> **Datatype:** String, selected from [available Protections](#available-protections)
|
| `method` | Protection name to use. <br> **Datatype:** String, selected from [available Protections](#available-protections) |
|
||||||
| `stop_duration_candles` | For how many candles should the lock be set? <br> **Datatype:** Positive integer (in candles)
|
| `stop_duration_candles` | For how many candles should the lock be set? <br> **Datatype:** Positive integer (in candles) |
|
||||||
| `stop_duration` | how many minutes should protections be locked. <br>Cannot be used together with `stop_duration_candles`. <br> **Datatype:** Float (in minutes)
|
| `stop_duration` | how many minutes should protections be locked. <br>Cannot be used together with `stop_duration_candles`. <br> **Datatype:** Float (in minutes) |
|
||||||
| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections. <br> **Datatype:** Positive integer (in candles).
|
| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections. <br> **Datatype:** Positive integer (in candles). |
|
||||||
| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered. <br>Cannot be used together with `lookback_period_candles`. <br>This setting may be ignored by some Protections. <br> **Datatype:** Float (in minutes)
|
| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered. <br>Cannot be used together with `lookback_period_candles`. <br>This setting may be ignored by some Protections. <br> **Datatype:** Float (in minutes) |
|
||||||
| `trade_limit` | Number of trades required at minimum (not used by all Protections). <br> **Datatype:** Positive integer
|
| `trade_limit` | Number of trades required at minimum (not used by all Protections). <br> **Datatype:** Positive integer |
|
||||||
| `unlock_at` | Time when trading will be unlocked regularly (not used by all Protections). <br> **Datatype:** string <br>**Input Format:** "HH:MM" (24-hours)
|
| `unlock_at` | Time when trading will be unlocked regularly (not used by all Protections). <br> **Datatype:** string <br>**Input Format:** "HH:MM" (24-hours) |
|
||||||
|
|
||||||
!!! Note "Durations"
|
!!! Note "Durations"
|
||||||
Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles).
|
Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles).
|
||||||
@@ -69,7 +69,17 @@ def protections(self):
|
|||||||
|
|
||||||
#### MaxDrawdown
|
#### MaxDrawdown
|
||||||
|
|
||||||
`MaxDrawdown` uses all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after the last trade - assuming that the bot needs some time to let markets recover.
|
The `MaxDrawdown` protection evaluates trades that closed within the current `lookback_period` (or `lookback_period_candles`).
|
||||||
|
It supports 2 calculation modes:
|
||||||
|
|
||||||
|
- `calculation_mode: "ratios"` (default): Legacy approximation based on cumulative profit ratios.
|
||||||
|
- `calculation_mode: "equity"`: Standard peak-to-trough drawdown on the account equity curve, using starting balance and cumulative absolute profit.
|
||||||
|
|
||||||
|
With `calculation_mode: "ratios"`, drawdown is derived from cumulative trade profit ratios, not from the account equity curve. This is kept for backward compatibility and can differ from account-level drawdown when position sizing changes over time.
|
||||||
|
|
||||||
|
For new setups, `calculation_mode: "equity"` is recommended. Prefer `calculation_mode: "ratios"` only when you intentionally rely on legacy behavior, especially with fixed stake amount configurations where ratio-based behavior is easier to reason about.
|
||||||
|
|
||||||
|
If the observed drawdown exceeds `max_allowed_drawdown`, trading will stop for `stop_duration` after the last trade - assuming that the bot needs some time to let markets recover.
|
||||||
|
|
||||||
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
|
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
|
||||||
|
|
||||||
@@ -79,6 +89,7 @@ def protections(self):
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"method": "MaxDrawdown",
|
"method": "MaxDrawdown",
|
||||||
|
"calculation_mode": "equity",
|
||||||
"lookback_period_candles": 48,
|
"lookback_period_candles": 48,
|
||||||
"trade_limit": 20,
|
"trade_limit": 20,
|
||||||
"stop_duration_candles": 12,
|
"stop_duration_candles": 12,
|
||||||
@@ -160,6 +171,7 @@ class AwesomeStrategy(IStrategy)
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "MaxDrawdown",
|
"method": "MaxDrawdown",
|
||||||
|
"calculation_mode": "equity",
|
||||||
"lookback_period_candles": 48,
|
"lookback_period_candles": 48,
|
||||||
"trade_limit": 20,
|
"trade_limit": 20,
|
||||||
"stop_duration_candles": 4,
|
"stop_duration_candles": 4,
|
||||||
|
|||||||
+4
-1
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
[](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
|
[](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
|
||||||
[](https://doi.org/10.21105/joss.04864)
|
[](https://doi.org/10.21105/joss.04864)
|
||||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[](https://codecov.io/gh/freqtrade/freqtrade)
|
||||||
|
[](https://www.freqtrade.io)
|
||||||
|
[](https://discord.gg/p7nuUNVfP7)
|
||||||
|
|
||||||
<!-- GitHub action buttons -->
|
<!-- GitHub action buttons -->
|
||||||
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }
|
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }
|
||||||
@@ -60,6 +62,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
|||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
|
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
|
||||||
- [X] [OKX](https://okx.com/)
|
- [X] [OKX](https://okx.com/)
|
||||||
|
- [X] [Kraken](https://www.kraken.com/features/futures)
|
||||||
|
|
||||||
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.
|
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
markdown==3.10
|
markdown==3.10.2
|
||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
mkdocs-material==9.7.1
|
mkdocs-material==9.7.5
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.20
|
pymdown-extensions==10.21
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
mike==2.1.3
|
mike==2.1.4
|
||||||
|
|||||||
+3
-3
@@ -17,7 +17,7 @@ Sample configuration:
|
|||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
"verbosity": "error",
|
"verbosity": "error",
|
||||||
"enable_openapi": false,
|
"enable_openapi": false,
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": [],
|
"CORS_origins": [],
|
||||||
"username": "Freqtrader",
|
"username": "Freqtrader",
|
||||||
"password": "SuperSecret1!",
|
"password": "SuperSecret1!",
|
||||||
@@ -56,7 +56,7 @@ secrets.token_hex()
|
|||||||
|
|
||||||
!!! Danger "Password selection"
|
!!! Danger "Password selection"
|
||||||
Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
|
Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
|
||||||
Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!).
|
Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!). This value should also be 32 characters or longer to be safe.
|
||||||
|
|
||||||
### Configuration with docker
|
### Configuration with docker
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ You would then add that token under `ws_token` in your `api_server` config. Like
|
|||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
"verbosity": "error",
|
"verbosity": "error",
|
||||||
"enable_openapi": false,
|
"enable_openapi": false,
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": [],
|
"CORS_origins": [],
|
||||||
"username": "Freqtrader",
|
"username": "Freqtrader",
|
||||||
"password": "SuperSecret1!",
|
"password": "SuperSecret1!",
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ WHERE id=31;
|
|||||||
### Remove trade from the database
|
### Remove trade from the database
|
||||||
|
|
||||||
!!! Tip "Use RPC Methods to delete trades"
|
!!! Tip "Use RPC Methods to delete trades"
|
||||||
Consider using `/delete <tradeid>` via telegram or rest API. That's the recommended way to deleting trades.
|
Consider using `/delete <tradeid>` via telegram or rest API. That's the recommended way to deleting trades, as it will also remove the corresponding orders and custom data, and it will also trigger the necessary events in the bot to keep everything in sync.
|
||||||
|
|
||||||
If you'd still like to remove a trade from the database directly, you can use the below query.
|
If you'd still like to remove a trade from the database directly, you can use the below query.
|
||||||
|
|
||||||
@@ -113,9 +113,14 @@ If you'd still like to remove a trade from the database directly, you can use th
|
|||||||
|
|
||||||
```sql
|
```sql
|
||||||
DELETE FROM trades WHERE id = <tradeid>;
|
DELETE FROM trades WHERE id = <tradeid>;
|
||||||
|
DELETE FROM orders WHERE ft_trade_id = <tradeid>;
|
||||||
|
DELETE FROM trade_custom_data WHERE ft_trade_id = <tradeid>;
|
||||||
|
|
||||||
|
|
||||||
DELETE FROM trades WHERE id = 31;
|
DELETE FROM trades WHERE id = 31;
|
||||||
|
DELETE FROM orders WHERE ft_trade_id = 31;
|
||||||
|
DELETE FROM trade_custom_data WHERE ft_trade_id = 31;
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause.
|
This will remove the specified trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause.
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ This same logic will reapply a stoploss order on the exchange should you cancel
|
|||||||
`stoploss_price_type` only applies to futures markets (on exchanges where it's available).
|
`stoploss_price_type` only applies to futures markets (on exchanges where it's available).
|
||||||
Freqtrade will perform a validation of this setting on startup, failing to start if an invalid setting for your exchange has been selected.
|
Freqtrade will perform a validation of this setting on startup, failing to start if an invalid setting for your exchange has been selected.
|
||||||
Supported price types are gonna differs between each exchanges. Please check with your exchange on which price types it supports.
|
Supported price types are gonna differs between each exchanges. Please check with your exchange on which price types it supports.
|
||||||
|
In spot markets, this setting is ignored and not validated, as most exchanges only support one price type for stoploss orders on spot markets.
|
||||||
|
|
||||||
Stoploss on exchange on futures markets can trigger on different price types.
|
Stoploss on exchange on futures markets can trigger on different price types.
|
||||||
The naming for these prices in exchange terminology often varies, but is usually something around "last" (or "contract price" ), "mark" and "index".
|
The naming for these prices in exchange terminology often varies, but is usually something around "last" (or "contract price" ), "mark" and "index".
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
trade_entry_type = trade.get_custom_data(key='entry_type')
|
trade_entry_type = trade.get_custom_data(key='entry_type')
|
||||||
if trade_entry_type is None:
|
if trade_entry_type is None:
|
||||||
trade_entry_type = 'breakout' if 'entry_1' in trade.enter_tag else 'dip'
|
trade_entry_type = 'breakout' if 'entry_1' in trade.enter_tag else 'dip'
|
||||||
elif fills > 1:
|
elif len(fills) > 1:
|
||||||
trade_entry_type = 'buy_up'
|
trade_entry_type = 'buy_up'
|
||||||
trade.set_custom_data(key='entry_type', value=trade_entry_type)
|
trade.set_custom_data(key='entry_type', value=trade_entry_type)
|
||||||
return super().bot_loop_start(**kwargs)
|
return super().bot_loop_start(**kwargs)
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||||
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
|
||||||
|
|
||||||
When not implemented by a strategy, returns the initial stoploss value.
|
When not implemented by a strategy, returns the initial stoploss value.
|
||||||
Only called when use_custom_stoploss is set to True.
|
Only called when use_custom_stoploss is set to True.
|
||||||
@@ -696,6 +696,9 @@ However, freqtrade also offers a custom callback for both order types, which all
|
|||||||
Backtesting fills orders if their price falls within the candle's low/high range.
|
Backtesting fills orders if their price falls within the candle's low/high range.
|
||||||
The below callbacks will be called once per (detail) candle for orders that don't fill immediately (which use custom pricing).
|
The below callbacks will be called once per (detail) candle for orders that don't fill immediately (which use custom pricing).
|
||||||
|
|
||||||
|
!!! Tip "Replacing orders"
|
||||||
|
If you'd like to replace an order with a different price instead of just cancelling it, you might want to look at [`adjust_order_price()`](#adjust-order-price) instead, which will allow you to both cancel the order, as well as replace it with a new price.
|
||||||
|
|
||||||
### Custom order timeout example
|
### Custom order timeout example
|
||||||
|
|
||||||
Called for every open order until that order is either filled or cancelled.
|
Called for every open order until that order is either filled or cancelled.
|
||||||
@@ -805,7 +808,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
Timing for this function is critical, so avoid doing heavy computations or
|
Timing for this function is critical, so avoid doing heavy computations or
|
||||||
network requests in this method.
|
network requests in this method.
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
|
||||||
|
|
||||||
When not implemented by a strategy, returns True (always confirming).
|
When not implemented by a strategy, returns True (always confirming).
|
||||||
|
|
||||||
@@ -853,7 +856,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
Timing for this function is critical, so avoid doing heavy computations or
|
Timing for this function is critical, so avoid doing heavy computations or
|
||||||
network requests in this method.
|
network requests in this method.
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
|
||||||
|
|
||||||
When not implemented by a strategy, returns True (always confirming).
|
When not implemented by a strategy, returns True (always confirming).
|
||||||
|
|
||||||
@@ -991,7 +994,7 @@ class DigDeeperStrategy(IStrategy):
|
|||||||
This means extra entry or exit orders with additional fees.
|
This means extra entry or exit orders with additional fees.
|
||||||
Only called when `position_adjustment_enable` is set to True.
|
Only called when `position_adjustment_enable` is set to True.
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
|
||||||
|
|
||||||
When not implemented by a strategy, returns None
|
When not implemented by a strategy, returns None
|
||||||
|
|
||||||
@@ -1118,7 +1121,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
This only executes when a order was already placed, still open (unfilled fully or partially)
|
This only executes when a order was already placed, still open (unfilled fully or partially)
|
||||||
and not timed out on subsequent candles after entry trigger.
|
and not timed out on subsequent candles after entry trigger.
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
|
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-callbacks/
|
||||||
|
|
||||||
When not implemented by a strategy, returns current_order_rate as default.
|
When not implemented by a strategy, returns current_order_rate as default.
|
||||||
If current_order_rate is returned then the existing order is maintained.
|
If current_order_rate is returned then the existing order is maintained.
|
||||||
@@ -1303,7 +1306,8 @@ Currently two types of annotations are supported, `area` and `line`.
|
|||||||
"z_level": 5, // z-level, higher values are drawn on top of lower values. Positions relative to the Chart elements need to be set in freqUI.
|
"z_level": 5, // z-level, higher values are drawn on top of lower values. Positions relative to the Chart elements need to be set in freqUI.
|
||||||
"label": "some label",
|
"label": "some label",
|
||||||
"size": 2, // Optional, line width in pixels. Defaults to 10
|
"size": 2, // Optional, line width in pixels. Defaults to 10
|
||||||
"symbol": "circle", // Optional, can be "circle", "rect", "roundRect", "triangle", "pin", "arrow", "none".
|
"shape": "circle", // Optional, can be "circle", "rect", "roundRect", "triangle", "pin", "arrow", "none".
|
||||||
|
"rotate": 0, // Optional, rotation of the shape/symbol in degrees. Defaults to 0
|
||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -1385,7 +1389,7 @@ Entries will be validated, and won't be passed to the UI if they don't correspon
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif (start_dt.hour % 2) == 0:
|
elif (start_dt.hour % 2) == 0:
|
||||||
price = dataframe.loc[dataframe["date"] == start_dt, ["close"]].mean()
|
price = dataframe.loc[dataframe["date"] == start_dt, "close"].mean()
|
||||||
annotations.append(
|
annotations.append(
|
||||||
{
|
{
|
||||||
"type": "area",
|
"type": "area",
|
||||||
|
|||||||
@@ -594,9 +594,9 @@ Features will now expand automatically. As such, the expansion loops, as well as
|
|||||||
More details on how these config defined parameters accelerate feature engineering
|
More details on how these config defined parameters accelerate feature engineering
|
||||||
in the documentation at:
|
in the documentation at:
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
|
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
|
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
|
||||||
|
|
||||||
:param df: strategy dataframe which will receive the features
|
:param df: strategy dataframe which will receive the features
|
||||||
:param period: period of the indicator - usage example:
|
:param period: period of the indicator - usage example:
|
||||||
@@ -657,9 +657,9 @@ Basic features. Make sure to remove the `{pair}` part from your features.
|
|||||||
More details on how these config defined parameters accelerate feature engineering
|
More details on how these config defined parameters accelerate feature engineering
|
||||||
in the documentation at:
|
in the documentation at:
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
|
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
|
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
|
||||||
|
|
||||||
:param df: strategy dataframe which will receive the features
|
:param df: strategy dataframe which will receive the features
|
||||||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||||
@@ -690,7 +690,7 @@ Basic features. Make sure to remove the `{pair}` part from your features.
|
|||||||
|
|
||||||
More details about feature engineering available:
|
More details about feature engineering available:
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
https://www.freqtrade.io/en/stable/freqai-feature-engineering
|
||||||
|
|
||||||
:param df: strategy dataframe which will receive the features
|
:param df: strategy dataframe which will receive the features
|
||||||
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
|
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
|
||||||
@@ -713,7 +713,7 @@ Targets now get their own, dedicated method.
|
|||||||
|
|
||||||
More details about feature engineering available:
|
More details about feature engineering available:
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
https://www.freqtrade.io/en/stable/freqai-feature-engineering
|
||||||
|
|
||||||
:param df: strategy dataframe which will receive the targets
|
:param df: strategy dataframe which will receive the targets
|
||||||
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
||||||
|
|||||||
+1
-1
@@ -416,6 +416,6 @@ Your original strategy will remain available in the `user_data/strategies_orig_u
|
|||||||
|
|
||||||
!!! Warning "Conversion results"
|
!!! Warning "Conversion results"
|
||||||
Strategy updater will work on a "best effort" approach. Please do your due diligence and verify the results of the conversion.
|
Strategy updater will work on a "best effort" approach. Please do your due diligence and verify the results of the conversion.
|
||||||
We also recommend to run a python formatter (e.g. `black`) to format results in a sane manner.
|
We also recommend to run a python formatter (e.g. `ruff format`) to format results in a sane manner.
|
||||||
|
|
||||||
--8<-- "commands/strategy-updater.md"
|
--8<-- "commands/strategy-updater.md"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Freqtrade bot"""
|
"""Freqtrade bot"""
|
||||||
|
|
||||||
__version__ = "2026.2-dev"
|
__version__ = "2026.3"
|
||||||
|
|
||||||
if "dev" in __version__:
|
if "dev" in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -215,9 +215,7 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"--strategy-list",
|
"--strategy-list",
|
||||||
help="Provide a space-separated list of strategies to backtest. "
|
help="Provide a space-separated list of strategies to backtest. "
|
||||||
"Please note that timeframe needs to be set either in config "
|
"Please note that timeframe needs to be set either in config "
|
||||||
"or via command line. When using this together with `--export trades`, "
|
"or via command line. ",
|
||||||
"the strategy-name is injected into the filename "
|
|
||||||
"(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`",
|
|
||||||
nargs="+",
|
nargs="+",
|
||||||
),
|
),
|
||||||
"backtest_notes": Arg(
|
"backtest_notes": Arg(
|
||||||
@@ -240,6 +238,14 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"exportfilename": Arg(
|
"exportfilename": Arg(
|
||||||
"--backtest-filename",
|
"--backtest-filename",
|
||||||
"--export-filename",
|
"--export-filename",
|
||||||
|
fthelp={
|
||||||
|
"freqtrade backtesting": (
|
||||||
|
"DEPRECATED: This option is deprecated for backtesting and will be removed "
|
||||||
|
"in a future release. "
|
||||||
|
"Using a custom filename for backtest results is no longer supported. "
|
||||||
|
"Use `--backtest-directory` to specify the directory."
|
||||||
|
),
|
||||||
|
},
|
||||||
help="Use this filename for backtest results."
|
help="Use this filename for backtest results."
|
||||||
"Example: `--backtest-filename=backtest_results_2020-09-27_16-20-48.json`. "
|
"Example: `--backtest-filename=backtest_results_2020-09-27_16-20-48.json`. "
|
||||||
"Assumes either `user_data/backtest_results/` or `--export-directory` as base directory.",
|
"Assumes either `user_data/backtest_results/` or `--export-directory` as base directory.",
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ def start_list_trades_data(args: dict[str, Any]) -> None:
|
|||||||
end.strftime(DATETIME_PRINT_FORMAT),
|
end.strftime(DATETIME_PRINT_FORMAT),
|
||||||
str(length),
|
str(length),
|
||||||
)
|
)
|
||||||
for pair, start, end, length in sorted(paircombs1, key=lambda x: (x[0]))
|
for pair, start, end, length in sorted(paircombs1, key=lambda x: x[0])
|
||||||
],
|
],
|
||||||
("Pair", "Type", "From", "To", "Trades"),
|
("Pair", "Type", "From", "To", "Trades"),
|
||||||
summary=title,
|
summary=title,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ def start_convert_db(args: dict[str, Any]) -> None:
|
|||||||
|
|
||||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||||
from freqtrade.persistence import Order, Trade, init_db
|
from freqtrade.persistence import Order, Trade, init_db
|
||||||
|
from freqtrade.persistence.custom_data import _CustomData
|
||||||
|
from freqtrade.persistence.key_value_store import _KeyValueStoreModel
|
||||||
from freqtrade.persistence.migrations import set_sequence_ids
|
from freqtrade.persistence.migrations import set_sequence_ids
|
||||||
from freqtrade.persistence.pairlock import PairLock
|
from freqtrade.persistence.pairlock import PairLock
|
||||||
|
|
||||||
@@ -25,6 +27,8 @@ def start_convert_db(args: dict[str, Any]) -> None:
|
|||||||
|
|
||||||
trade_count = 0
|
trade_count = 0
|
||||||
pairlock_count = 0
|
pairlock_count = 0
|
||||||
|
kv_count = 0
|
||||||
|
custom_data_count = 0
|
||||||
for trade in Trade.get_trades():
|
for trade in Trade.get_trades():
|
||||||
trade_count += 1
|
trade_count += 1
|
||||||
make_transient(trade)
|
make_transient(trade)
|
||||||
@@ -41,16 +45,35 @@ def start_convert_db(args: dict[str, Any]) -> None:
|
|||||||
session_target.add(pairlock)
|
session_target.add(pairlock)
|
||||||
session_target.commit()
|
session_target.commit()
|
||||||
|
|
||||||
|
for kv in _KeyValueStoreModel.session.scalars(select(_KeyValueStoreModel)):
|
||||||
|
kv_count += 1
|
||||||
|
make_transient(kv)
|
||||||
|
session_target.add(kv)
|
||||||
|
session_target.commit()
|
||||||
|
|
||||||
|
for cd in _CustomData.session.scalars(select(_CustomData)):
|
||||||
|
custom_data_count += 1
|
||||||
|
make_transient(cd)
|
||||||
|
session_target.add(cd)
|
||||||
|
session_target.commit()
|
||||||
|
|
||||||
# Update sequences
|
# Update sequences
|
||||||
max_trade_id = session_target.scalar(select(func.max(Trade.id)))
|
max_trade_id = session_target.scalar(select(func.max(Trade.id)))
|
||||||
max_order_id = session_target.scalar(select(func.max(Order.id)))
|
max_order_id = session_target.scalar(select(func.max(Order.id)))
|
||||||
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
|
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
|
||||||
|
max_kv_id = session_target.scalar(select(func.max(_KeyValueStoreModel.id)))
|
||||||
|
max_custom_data_id = session_target.scalar(select(func.max(_CustomData.id)))
|
||||||
|
|
||||||
set_sequence_ids(
|
set_sequence_ids(
|
||||||
session_target.get_bind(),
|
session_target.get_bind(),
|
||||||
trade_id=max_trade_id,
|
trade_id=(max_trade_id or 0) + 1,
|
||||||
order_id=max_order_id,
|
order_id=(max_order_id or 0) + 1,
|
||||||
pairlock_id=max_pairlock_id,
|
pairlock_id=(max_pairlock_id or 0) + 1,
|
||||||
|
kv_id=(max_kv_id or 0) + 1,
|
||||||
|
custom_data_id=(max_custom_data_id or 0) + 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Migrated {trade_count} Trades, and {pairlock_count} Pairlocks.")
|
logger.info(
|
||||||
|
f"Migrated {trade_count} Trades, {pairlock_count} Pairlocks, "
|
||||||
|
f"{kv_count} Key-Value pairs, and {custom_data_count} Custom Data entries."
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import sys
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
from freqtrade.exceptions import ConfigurationError, DependencyException, OperationalException
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -166,7 +166,14 @@ def start_list_strategies(args: dict[str, Any]) -> None:
|
|||||||
strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
|
strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
|
||||||
for obj in strategy_objs:
|
for obj in strategy_objs:
|
||||||
if obj["class"]:
|
if obj["class"]:
|
||||||
obj["hyperoptable"] = detect_all_parameters(obj["class"])
|
try:
|
||||||
|
obj["hyperoptable"] = detect_all_parameters(obj["class"])
|
||||||
|
except DependencyException as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Cannot detect hyperoptable parameters for strategy {obj['name']}. Reason: {e}"
|
||||||
|
)
|
||||||
|
obj["hyperoptable"] = {}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
obj["hyperoptable"] = {}
|
obj["hyperoptable"] = {}
|
||||||
|
|
||||||
|
|||||||
@@ -752,6 +752,7 @@ CONF_SCHEMA = {
|
|||||||
"jwt_secret_key": {
|
"jwt_secret_key": {
|
||||||
"description": "Secret key for JWT authentication.",
|
"description": "Secret key for JWT authentication.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"default": "somethingRandomSomethingRandom123",
|
||||||
},
|
},
|
||||||
"CORS_origins": {
|
"CORS_origins": {
|
||||||
"description": "List of allowed CORS origins.",
|
"description": "List of allowed CORS origins.",
|
||||||
@@ -764,7 +765,14 @@ CONF_SCHEMA = {
|
|||||||
"enum": ["error", "info"],
|
"enum": ["error", "info"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["enabled", "listen_ip_address", "listen_port", "username", "password"],
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
"listen_ip_address",
|
||||||
|
"listen_port",
|
||||||
|
"username",
|
||||||
|
"password",
|
||||||
|
"jwt_secret_key",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
# end of RPC section
|
# end of RPC section
|
||||||
"db_url": {
|
"db_url": {
|
||||||
|
|||||||
@@ -221,30 +221,30 @@ class Configuration:
|
|||||||
config, argname="exportfilename", logstring="Storing backtest results to {} ..."
|
config, argname="exportfilename", logstring="Storing backtest results to {} ..."
|
||||||
)
|
)
|
||||||
config["exportfilename"] = Path(config["exportfilename"])
|
config["exportfilename"] = Path(config["exportfilename"])
|
||||||
if config.get("exportdirectory") and Path(config["exportdirectory"]).is_dir():
|
if config.get("exportfilename"):
|
||||||
logger.warning(
|
if Path(config["exportfilename"]).is_dir():
|
||||||
"DEPRECATED: Using `--export-filename` with directories is deprecated, "
|
logger.warning(
|
||||||
"use `--backtest-directory` instead."
|
"DEPRECATED: Using `--export-filename` with directories is deprecated, "
|
||||||
)
|
"use `--backtest-directory` instead."
|
||||||
if config.get("exportdirectory") is None:
|
)
|
||||||
# Fallback - assign export-directory directly.
|
if config.get("exportdirectory") is None:
|
||||||
config["exportdirectory"] = config["exportfilename"]
|
# Fallback - assign export-directory directly.
|
||||||
|
config["exportdirectory"] = config["exportfilename"]
|
||||||
|
elif config.get("runmode") == RunMode.BACKTEST:
|
||||||
|
logger.warning(
|
||||||
|
"DEPRECATED: Using `--export-filename` has no impact when backtesting. "
|
||||||
|
"Please use `--notes` to annotate backtest results and "
|
||||||
|
"`--backtest-directory` to specify the output directory. "
|
||||||
|
)
|
||||||
if not config.get("exportdirectory"):
|
if not config.get("exportdirectory"):
|
||||||
config["exportdirectory"] = config["user_data_dir"] / "backtest_results"
|
config["exportdirectory"] = config["user_data_dir"] / "backtest_results"
|
||||||
if not config.get("exportfilename"):
|
|
||||||
config["exportfilename"] = None
|
config["exportfilename"] = config.get("exportfilename", None)
|
||||||
if config.get("exportfilename"):
|
if config.get("exportfilename"):
|
||||||
# ensure exportfilename is a Path object
|
# ensure exportfilename is a Path object
|
||||||
config["exportfilename"] = Path(config["exportfilename"])
|
config["exportfilename"] = Path(config["exportfilename"])
|
||||||
config["exportdirectory"] = Path(config["exportdirectory"])
|
config["exportdirectory"] = Path(config["exportdirectory"])
|
||||||
|
|
||||||
if self.args.get("show_sensitive"):
|
|
||||||
logger.warning(
|
|
||||||
"Sensitive information will be shown in the upcoming output. "
|
|
||||||
"Please make sure to never share this output without redacting "
|
|
||||||
"the information yourself."
|
|
||||||
)
|
|
||||||
|
|
||||||
def _process_optimize_options(self, config: Config) -> None:
|
def _process_optimize_options(self, config: Config) -> None:
|
||||||
# This will override the strategy configuration
|
# This will override the strategy configuration
|
||||||
self._args_to_config(
|
self._args_to_config(
|
||||||
@@ -312,6 +312,13 @@ class Configuration:
|
|||||||
|
|
||||||
self._process_datadir_options(config)
|
self._process_datadir_options(config)
|
||||||
|
|
||||||
|
if self.args.get("show_sensitive"):
|
||||||
|
logger.warning(
|
||||||
|
"Sensitive information will be shown in the upcoming output. "
|
||||||
|
"Please make sure to never share this output without redacting "
|
||||||
|
"the information yourself."
|
||||||
|
)
|
||||||
|
|
||||||
self._args_to_config(
|
self._args_to_config(
|
||||||
config,
|
config,
|
||||||
argname="strategy_list",
|
argname="strategy_list",
|
||||||
@@ -403,7 +410,7 @@ class Configuration:
|
|||||||
("include_inactive", "Detected --include-inactive-pairs: {}"),
|
("include_inactive", "Detected --include-inactive-pairs: {}"),
|
||||||
("no_parallel_download", "Detected --no-parallel-download: {}"),
|
("no_parallel_download", "Detected --no-parallel-download: {}"),
|
||||||
("download_trades", "Detected --dl-trades: {}"),
|
("download_trades", "Detected --dl-trades: {}"),
|
||||||
("convert_trades", "Detected --convert: {} - Converting Trade data to OHCV {}"),
|
("convert_trades", "Detected --convert: {} - Converting trade data to OHLCV."),
|
||||||
("dataformat_ohlcv", 'Using "{}" to store OHLCV data.'),
|
("dataformat_ohlcv", 'Using "{}" to store OHLCV data.'),
|
||||||
("dataformat_trades", 'Using "{}" to store trades data.'),
|
("dataformat_trades", 'Using "{}" to store trades data.'),
|
||||||
("show_timerange", "Detected --show-timerange"),
|
("show_timerange", "Detected --show-timerange"),
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ AVAILABLE_PAIRLISTS = [
|
|||||||
"ProducerPairList",
|
"ProducerPairList",
|
||||||
"RemotePairList",
|
"RemotePairList",
|
||||||
"MarketCapPairList",
|
"MarketCapPairList",
|
||||||
|
"CrossMarketPairList",
|
||||||
"AgeFilter",
|
"AgeFilter",
|
||||||
"DelistFilter",
|
"DelistFilter",
|
||||||
"FullTradesFilter",
|
"FullTradesFilter",
|
||||||
@@ -239,3 +240,6 @@ IntOrInf = float
|
|||||||
|
|
||||||
|
|
||||||
EntryExecuteMode = Literal["initial", "pos_adjust", "replace"]
|
EntryExecuteMode = Literal["initial", "pos_adjust", "replace"]
|
||||||
|
|
||||||
|
# Prefixes for low-priced coins like 1000PEPE/USDDT:USDT or KPEPE/USDC (hyperliquid)
|
||||||
|
PairPrefixes = ["1000", "1000000", "1M", "K"]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from numpy import format_float_positional
|
||||||
from pandas import DataFrame, Series
|
from pandas import DataFrame, Series
|
||||||
|
|
||||||
|
|
||||||
@@ -11,7 +12,10 @@ def get_tick_size_over_time(candles: DataFrame) -> Series:
|
|||||||
# count the number of significant digits for the open and close prices
|
# count the number of significant digits for the open and close prices
|
||||||
for col in ["open", "high", "low", "close"]:
|
for col in ["open", "high", "low", "close"]:
|
||||||
candles[f"{col}_count"] = (
|
candles[f"{col}_count"] = (
|
||||||
candles[col].round(14).apply("{:.15f}".format).str.extract(r"\.(\d*[1-9])")[0].str.len()
|
candles[col]
|
||||||
|
.apply(format_float_positional, precision=14, unique=False, fractional=False, trim="-")
|
||||||
|
.str.extract(r"\.(\d*[1-9])")[0]
|
||||||
|
.str.len()
|
||||||
)
|
)
|
||||||
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
|
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
|
||||||
axis=1
|
axis=1
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ def ohlcv_to_dataframe(
|
|||||||
df = DataFrame(ohlcv, columns=cols)
|
df = DataFrame(ohlcv, columns=cols)
|
||||||
|
|
||||||
# Floor date to seconds to account for exchange imprecisions
|
# Floor date to seconds to account for exchange imprecisions
|
||||||
df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor("s")
|
from freqtrade.exchange import timeframe_to_floor_freq
|
||||||
|
|
||||||
|
resample_interval = timeframe_to_floor_freq(timeframe)
|
||||||
|
|
||||||
|
df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor(resample_interval)
|
||||||
|
|
||||||
# Some exchanges return int values for Volume and even for OHLC.
|
# Some exchanges return int values for Volume and even for OHLC.
|
||||||
# Convert them since TA-LIB indicators used in the strategy assume floats
|
# Convert them since TA-LIB indicators used in the strategy assume floats
|
||||||
@@ -59,14 +63,14 @@ def ohlcv_to_dataframe(
|
|||||||
|
|
||||||
|
|
||||||
def clean_ohlcv_dataframe(
|
def clean_ohlcv_dataframe(
|
||||||
data: DataFrame, timeframe: str, pair: str, *, fill_missing: bool, drop_incomplete: bool
|
dataframe: DataFrame, timeframe: str, pair: str, *, fill_missing: bool, drop_incomplete: bool
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Cleanse a OHLCV dataframe by
|
Cleanse a OHLCV dataframe by
|
||||||
* Grouping it by date (removes duplicate tics)
|
* Grouping it by date (removes duplicate tics)
|
||||||
* dropping last candles if requested
|
* dropping last candles if requested
|
||||||
* Filling up missing data (if requested)
|
* Filling up missing data (if requested)
|
||||||
:param data: DataFrame containing candle (OHLCV) data.
|
:param dataframe: DataFrame containing candle (OHLCV) data.
|
||||||
:param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data
|
:param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data
|
||||||
:param pair: Pair this data is for (used to warn if fillup was necessary)
|
:param pair: Pair this data is for (used to warn if fillup was necessary)
|
||||||
:param fill_missing: fill up missing candles with 0 candles
|
:param fill_missing: fill up missing candles with 0 candles
|
||||||
@@ -75,7 +79,7 @@ def clean_ohlcv_dataframe(
|
|||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
# group by index and aggregate results to eliminate duplicate ticks
|
# group by index and aggregate results to eliminate duplicate ticks
|
||||||
data = data.groupby(by="date", as_index=False, sort=True).agg(
|
dataframe = dataframe.groupby(by="date", as_index=False, sort=True).agg(
|
||||||
{
|
{
|
||||||
"open": "first",
|
"open": "first",
|
||||||
"high": "max",
|
"high": "max",
|
||||||
@@ -86,13 +90,13 @@ def clean_ohlcv_dataframe(
|
|||||||
)
|
)
|
||||||
# eliminate partial candle
|
# eliminate partial candle
|
||||||
if drop_incomplete:
|
if drop_incomplete:
|
||||||
data.drop(data.tail(1).index, inplace=True)
|
dataframe.drop(dataframe.tail(1).index, inplace=True)
|
||||||
logger.debug("Dropping last candle")
|
logger.debug("Dropping last candle")
|
||||||
|
|
||||||
if fill_missing:
|
if fill_missing:
|
||||||
return ohlcv_fill_up_missing_data(data, timeframe, pair)
|
return ohlcv_fill_up_missing_data(dataframe, timeframe, pair)
|
||||||
else:
|
else:
|
||||||
return data
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame:
|
def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame:
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class IDataHandler(ABC):
|
class IDataHandler(ABC):
|
||||||
_OHLCV_REGEX = r"^([a-zA-Z_\d-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)"
|
_OHLCV_REGEX = r"^([\w-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)"
|
||||||
_TRADES_REGEX = r"^([a-zA-Z_\d-]+)\-(trades)?(?=\.)"
|
_TRADES_REGEX = r"^([\w-]+)\-(trades)?(?=\.)"
|
||||||
|
|
||||||
def __init__(self, datadir: Path) -> None:
|
def __init__(self, datadir: Path) -> None:
|
||||||
self._datadir = datadir
|
self._datadir = datadir
|
||||||
@@ -70,28 +70,6 @@ class IDataHandler(ABC):
|
|||||||
if match and len(match.groups()) > 1
|
if match and len(match.groups()) > 1
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> list[str]:
|
|
||||||
"""
|
|
||||||
Returns a list of all pairs with ohlcv data available in this datadir
|
|
||||||
for the specified timeframe
|
|
||||||
:param datadir: Directory to search for ohlcv files
|
|
||||||
:param timeframe: Timeframe to search pairs for
|
|
||||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
|
||||||
:return: List of Pairs
|
|
||||||
"""
|
|
||||||
candle = ""
|
|
||||||
if candle_type != CandleType.SPOT:
|
|
||||||
datadir = datadir.joinpath("futures")
|
|
||||||
candle = f"-{candle_type}"
|
|
||||||
ext = cls._get_file_extension()
|
|
||||||
_tmp = [
|
|
||||||
re.search(r"^(\S+)(?=\-" + timeframe + candle + f".{ext})", p.name)
|
|
||||||
for p in datadir.glob(f"*{timeframe}{candle}.{ext}")
|
|
||||||
]
|
|
||||||
# Check if regex found something and only return these results
|
|
||||||
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def ohlcv_store(
|
def ohlcv_store(
|
||||||
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
|
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
|
||||||
@@ -358,11 +336,10 @@ class IDataHandler(ABC):
|
|||||||
def rebuild_pair_from_filename(pair: str) -> str:
|
def rebuild_pair_from_filename(pair: str) -> str:
|
||||||
"""
|
"""
|
||||||
Rebuild pair name from filename
|
Rebuild pair name from filename
|
||||||
Assumes a asset name of max. 7 length to also support BTC-PERP and BTC-PERP:USD names.
|
Replaces the first '_' with '/' and the second '_' (if present) with ':'.
|
||||||
|
e.g. BTC_USDT -> BTC/USDT, BTC_USDT_USDT -> BTC/USDT:USDT
|
||||||
"""
|
"""
|
||||||
res = re.sub(r"^(([A-Za-z\d]{1,10})|^([A-Za-z\-]{1,6}))(_)", r"\g<1>/", pair, count=1)
|
return pair.replace("_", "/", 1).replace("_", ":", 1)
|
||||||
res = re.sub("_", ":", res, count=1)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def ohlcv_load(
|
def ohlcv_load(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
|
|||||||
:param final_balance: Final balance to calculate CAGR against
|
:param final_balance: Final balance to calculate CAGR against
|
||||||
:return: CAGR
|
:return: CAGR
|
||||||
"""
|
"""
|
||||||
if final_balance < 0:
|
if (final_balance < 0) or (starting_balance <= 0) or (days_passed <= 0):
|
||||||
# With leveraged trades, final_balance can become negative.
|
# With leveraged trades, final_balance can become negative.
|
||||||
return 0
|
return 0
|
||||||
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class CandleType(str, Enum):
|
class CandleType(StrEnum):
|
||||||
"""Enum to distinguish candle types"""
|
"""Enum to distinguish candle types"""
|
||||||
|
|
||||||
SPOT = "spot"
|
SPOT = "spot"
|
||||||
@@ -14,9 +14,6 @@ class CandleType(str, Enum):
|
|||||||
FUNDING_RATE = "funding_rate"
|
FUNDING_RATE = "funding_rate"
|
||||||
# BORROW_RATE = "borrow_rate" # * unimplemented
|
# BORROW_RATE = "borrow_rate" # * unimplemented
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name.lower()}"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_string(value: str) -> "CandleType":
|
def from_string(value: str) -> "CandleType":
|
||||||
if not value:
|
if not value:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class MarginMode(str, Enum):
|
class MarginMode(StrEnum):
|
||||||
"""
|
"""
|
||||||
Enum to distinguish between
|
Enum to distinguish between
|
||||||
cross margin/futures margin_mode and
|
cross margin/futures margin_mode and
|
||||||
@@ -11,6 +11,3 @@ class MarginMode(str, Enum):
|
|||||||
CROSS = "cross"
|
CROSS = "cross"
|
||||||
ISOLATED = "isolated"
|
ISOLATED = "isolated"
|
||||||
NONE = ""
|
NONE = ""
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.value.lower()}"
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class OrderTypeValues(str, Enum):
|
class OrderTypeValues(StrEnum):
|
||||||
limit = "limit"
|
limit = "limit"
|
||||||
market = "market"
|
market = "market"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class PriceType(str, Enum):
|
class PriceType(StrEnum):
|
||||||
"""Enum to distinguish possible trigger prices for stoplosses"""
|
"""Enum to distinguish possible trigger prices for stoplosses"""
|
||||||
|
|
||||||
LAST = "last"
|
LAST = "last"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class RPCMessageType(str, Enum):
|
class RPCMessageType(StrEnum):
|
||||||
STATUS = "status"
|
STATUS = "status"
|
||||||
WARNING = "warning"
|
WARNING = "warning"
|
||||||
EXCEPTION = "exception"
|
EXCEPTION = "exception"
|
||||||
@@ -25,21 +25,16 @@ class RPCMessageType(str, Enum):
|
|||||||
NEW_CANDLE = "new_candle"
|
NEW_CANDLE = "new_candle"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.value
|
# TODO: do we still need to overwrite __repr__? Impact needs to be looked at in detail
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
# Enum for parsing requests from ws consumers
|
# Enum for parsing requests from ws consumers
|
||||||
class RPCRequestType(str, Enum):
|
class RPCRequestType(StrEnum):
|
||||||
SUBSCRIBE = "subscribe"
|
SUBSCRIBE = "subscribe"
|
||||||
|
|
||||||
WHITELIST = "whitelist"
|
WHITELIST = "whitelist"
|
||||||
ANALYZED_DF = "analyzed_df"
|
ANALYZED_DF = "analyzed_df"
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
|
|
||||||
NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE)
|
NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class RunMode(str, Enum):
|
class RunMode(StrEnum):
|
||||||
"""
|
"""
|
||||||
Bot running mode (backtest, hyperopt, ...)
|
Bot running mode (backtest, hyperopt, ...)
|
||||||
can be "live", "dry-run", "backtest", "hyperopt".
|
can be "live", "dry-run", "backtest", "hyperopt".
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class SignalType(Enum):
|
class SignalType(StrEnum):
|
||||||
"""
|
"""
|
||||||
Enum to distinguish between enter and exit signals
|
Enum to distinguish between enter and exit signals
|
||||||
"""
|
"""
|
||||||
@@ -11,11 +11,8 @@ class SignalType(Enum):
|
|||||||
ENTER_SHORT = "enter_short"
|
ENTER_SHORT = "enter_short"
|
||||||
EXIT_SHORT = "exit_short"
|
EXIT_SHORT = "exit_short"
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name.lower()}"
|
|
||||||
|
|
||||||
|
class SignalTagType(StrEnum):
|
||||||
class SignalTagType(Enum):
|
|
||||||
"""
|
"""
|
||||||
Enum for signal columns
|
Enum for signal columns
|
||||||
"""
|
"""
|
||||||
@@ -23,13 +20,7 @@ class SignalTagType(Enum):
|
|||||||
ENTER_TAG = "enter_tag"
|
ENTER_TAG = "enter_tag"
|
||||||
EXIT_TAG = "exit_tag"
|
EXIT_TAG = "exit_tag"
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name.lower()}"
|
|
||||||
|
|
||||||
|
class SignalDirection(StrEnum):
|
||||||
class SignalDirection(str, Enum):
|
|
||||||
LONG = "long"
|
LONG = "long"
|
||||||
SHORT = "short"
|
SHORT = "short"
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name.lower()}"
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class TradingMode(str, Enum):
|
class TradingMode(StrEnum):
|
||||||
"""
|
"""
|
||||||
Enum to distinguish between
|
Enum to distinguish between
|
||||||
spot, margin, futures or any other trading method
|
spot, margin, futures or any other trading method
|
||||||
@@ -10,6 +10,3 @@ class TradingMode(str, Enum):
|
|||||||
SPOT = "spot"
|
SPOT = "spot"
|
||||||
MARGIN = "margin"
|
MARGIN = "margin"
|
||||||
FUTURES = "futures"
|
FUTURES = "futures"
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name.lower()}"
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from freqtrade.exchange.exchange_utils import (
|
|||||||
validate_exchange,
|
validate_exchange,
|
||||||
)
|
)
|
||||||
from freqtrade.exchange.exchange_utils_timeframe import (
|
from freqtrade.exchange.exchange_utils_timeframe import (
|
||||||
|
timeframe_to_floor_freq,
|
||||||
timeframe_to_minutes,
|
timeframe_to_minutes,
|
||||||
timeframe_to_msecs,
|
timeframe_to_msecs,
|
||||||
timeframe_to_next_date,
|
timeframe_to_next_date,
|
||||||
@@ -43,6 +44,7 @@ from freqtrade.exchange.htx import Htx
|
|||||||
from freqtrade.exchange.hyperliquid import Hyperliquid
|
from freqtrade.exchange.hyperliquid import Hyperliquid
|
||||||
from freqtrade.exchange.idex import Idex
|
from freqtrade.exchange.idex import Idex
|
||||||
from freqtrade.exchange.kraken import Kraken
|
from freqtrade.exchange.kraken import Kraken
|
||||||
|
from freqtrade.exchange.krakenfutures import Krakenfutures
|
||||||
from freqtrade.exchange.kucoin import Kucoin
|
from freqtrade.exchange.kucoin import Kucoin
|
||||||
from freqtrade.exchange.lbank import Lbank
|
from freqtrade.exchange.lbank import Lbank
|
||||||
from freqtrade.exchange.luno import Luno
|
from freqtrade.exchange.luno import Luno
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class Binance(Exchange):
|
|||||||
"has_delisting": True,
|
"has_delisting": True,
|
||||||
}
|
}
|
||||||
_ft_has_futures: FtHas = {
|
_ft_has_futures: FtHas = {
|
||||||
|
"ohlcv_candle_limit": 499,
|
||||||
"funding_fee_candle_limit": 1000,
|
"funding_fee_candle_limit": 1000,
|
||||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||||
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
||||||
|
|||||||
+39421
-37302
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
|
|||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.exceptions import (
|
from freqtrade.exceptions import (
|
||||||
DDosProtection,
|
DDosProtection,
|
||||||
OperationalException,
|
OperationalException,
|
||||||
@@ -34,6 +34,11 @@ class Bitget(Exchange):
|
|||||||
"stoploss_query_requires_stop_flag": True,
|
"stoploss_query_requires_stop_flag": True,
|
||||||
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
|
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
|
||||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||||
|
"stop_price_type_field": "triggerType",
|
||||||
|
"stop_price_type_value_mapping": {
|
||||||
|
PriceType.LAST: "fill_price",
|
||||||
|
PriceType.MARK: "mark_price",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
_ft_has_futures: FtHas = {
|
_ft_has_futures: FtHas = {
|
||||||
"funding_fee_candle_limit": 100,
|
"funding_fee_candle_limit": 100,
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ BAD_EXCHANGES = {
|
|||||||
"bitmex": "Various reasons",
|
"bitmex": "Various reasons",
|
||||||
"probit": "Requires additional, regular calls to `signIn()`",
|
"probit": "Requires additional, regular calls to `signIn()`",
|
||||||
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders",
|
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders",
|
||||||
"krakenfutures": "Unsupported futures exchange",
|
|
||||||
"kucoinfutures": "Unsupported futures exchange",
|
"kucoinfutures": "Unsupported futures exchange",
|
||||||
"poloniexfutures": "Unsupported futures exchange",
|
"poloniexfutures": "Unsupported futures exchange",
|
||||||
"binancecoinm": "Unsupported futures exchange",
|
"binancecoinm": "Unsupported futures exchange",
|
||||||
@@ -63,6 +62,7 @@ SUPPORTED_EXCHANGES = [
|
|||||||
"htx",
|
"htx",
|
||||||
"hyperliquid",
|
"hyperliquid",
|
||||||
"kraken",
|
"kraken",
|
||||||
|
"krakenfutures",
|
||||||
"okx",
|
"okx",
|
||||||
"myokx",
|
"myokx",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ from freqtrade.misc import (
|
|||||||
file_dump_json,
|
file_dump_json,
|
||||||
file_load_json,
|
file_load_json,
|
||||||
safe_value_fallback,
|
safe_value_fallback,
|
||||||
|
safe_value_nested,
|
||||||
)
|
)
|
||||||
from freqtrade.util import FtTTLCache, PeriodicCache, dt_from_ts, dt_now
|
from freqtrade.util import FtTTLCache, PeriodicCache, dt_from_ts, dt_now
|
||||||
from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time
|
from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time
|
||||||
@@ -207,7 +208,7 @@ class Exchange:
|
|||||||
self._config.get("trading_mode", self._supported_trading_mode_margin_pairs[0][0])
|
self._config.get("trading_mode", self._supported_trading_mode_margin_pairs[0][0])
|
||||||
)
|
)
|
||||||
self.margin_mode: MarginMode = MarginMode(
|
self.margin_mode: MarginMode = MarginMode(
|
||||||
MarginMode(self._config.get("margin_mode"))
|
self._config["margin_mode"]
|
||||||
if self._config.get("margin_mode")
|
if self._config.get("margin_mode")
|
||||||
else self._supported_trading_mode_margin_pairs[0][1]
|
else self._supported_trading_mode_margin_pairs[0][1]
|
||||||
)
|
)
|
||||||
@@ -313,10 +314,19 @@ class Exchange:
|
|||||||
if self._exchange_ws:
|
if self._exchange_ws:
|
||||||
self._exchange_ws.cleanup()
|
self._exchange_ws.cleanup()
|
||||||
logger.debug("Exchange object destroyed, closing async loop")
|
logger.debug("Exchange object destroyed, closing async loop")
|
||||||
|
try:
|
||||||
|
generic_loop = asyncio.get_running_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
generic_loop = None
|
||||||
|
loop_running = (getattr(self, "loop", None) and self.loop.is_running()) or (
|
||||||
|
generic_loop is not None and generic_loop.is_running()
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
getattr(self, "_api_async", None)
|
getattr(self, "_api_async", None)
|
||||||
and inspect.iscoroutinefunction(self._api_async.close)
|
and inspect.iscoroutinefunction(self._api_async.close)
|
||||||
and self._api_async.session
|
and self._api_async.session
|
||||||
|
and not loop_running
|
||||||
):
|
):
|
||||||
logger.debug("Closing async ccxt session.")
|
logger.debug("Closing async ccxt session.")
|
||||||
self.loop.run_until_complete(self._api_async.close())
|
self.loop.run_until_complete(self._api_async.close())
|
||||||
@@ -324,6 +334,7 @@ class Exchange:
|
|||||||
self._ws_async
|
self._ws_async
|
||||||
and inspect.iscoroutinefunction(self._ws_async.close)
|
and inspect.iscoroutinefunction(self._ws_async.close)
|
||||||
and self._ws_async.session
|
and self._ws_async.session
|
||||||
|
and not loop_running
|
||||||
):
|
):
|
||||||
logger.debug("Closing ws ccxt session.")
|
logger.debug("Closing ws ccxt session.")
|
||||||
self.loop.run_until_complete(self._ws_async.close())
|
self.loop.run_until_complete(self._ws_async.close())
|
||||||
@@ -814,7 +825,8 @@ class Exchange:
|
|||||||
and order_types["stoploss_price_type"] not in price_mapping
|
and order_types["stoploss_price_type"] not in price_mapping
|
||||||
):
|
):
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
f"On exchange stoploss price type is not supported for {self.name}."
|
f"On exchange stoploss price type '{order_types['stoploss_price_type']}' "
|
||||||
|
f"is not supported for {self.name}."
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_pricing(self, pricing: dict) -> None:
|
def validate_pricing(self, pricing: dict) -> None:
|
||||||
@@ -982,12 +994,12 @@ class Exchange:
|
|||||||
swap.linear.fetchOHLCV.limit
|
swap.linear.fetchOHLCV.limit
|
||||||
"""
|
"""
|
||||||
feat = (
|
feat = (
|
||||||
self._api_async.features.get("spot", {})
|
safe_value_nested(self._api_async.features, "spot", {})
|
||||||
if market_type == "spot"
|
if market_type == "spot"
|
||||||
else self._api_async.features.get("swap", {}).get("linear", {})
|
else safe_value_nested(self._api_async.features, "swap.linear", {})
|
||||||
)
|
)
|
||||||
|
|
||||||
return feat.get(endpoint, {}).get(attribute, default)
|
return safe_value_nested(feat, f"{endpoint}.{attribute}", default)
|
||||||
|
|
||||||
def get_precision_amount(self, pair: str) -> float | None:
|
def get_precision_amount(self, pair: str) -> float | None:
|
||||||
"""
|
"""
|
||||||
@@ -1156,7 +1168,7 @@ class Exchange:
|
|||||||
orderbook: OrderBook | None = None
|
orderbook: OrderBook | None = None
|
||||||
if self.exchange_has("fetchL2OrderBook"):
|
if self.exchange_has("fetchL2OrderBook"):
|
||||||
orderbook = self.fetch_l2_order_book(pair, 20)
|
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||||
if ordertype == "limit" and orderbook:
|
if not stop_loss and ordertype == "limit" and orderbook:
|
||||||
# Allow a 1% price difference
|
# Allow a 1% price difference
|
||||||
allowed_diff = 0.01
|
allowed_diff = 0.01
|
||||||
if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
|
if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
|
||||||
@@ -1293,6 +1305,7 @@ class Exchange:
|
|||||||
Check dry-run limit order fill and update fee (if it filled).
|
Check dry-run limit order fill and update fee (if it filled).
|
||||||
"""
|
"""
|
||||||
if order["status"] != "closed" and order.get("ft_order_type") == "stoploss":
|
if order["status"] != "closed" and order.get("ft_order_type") == "stoploss":
|
||||||
|
# Stoploss branch
|
||||||
pair = order["symbol"]
|
pair = order["symbol"]
|
||||||
if not orderbook and self.exchange_has("fetchL2OrderBook"):
|
if not orderbook and self.exchange_has("fetchL2OrderBook"):
|
||||||
orderbook = self.fetch_l2_order_book(pair, 20)
|
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||||
@@ -1300,6 +1313,11 @@ class Exchange:
|
|||||||
crossed = self._dry_is_price_crossed(
|
crossed = self._dry_is_price_crossed(
|
||||||
pair, order["side"], price, orderbook, is_stop=True
|
pair, order["side"], price, orderbook, is_stop=True
|
||||||
)
|
)
|
||||||
|
if crossed and immediate:
|
||||||
|
raise InvalidOrderException(
|
||||||
|
"Could not create dry stoploss order. Stoploss would trigger immediately."
|
||||||
|
)
|
||||||
|
|
||||||
if crossed:
|
if crossed:
|
||||||
average = self.get_dry_market_fill_price(
|
average = self.get_dry_market_fill_price(
|
||||||
pair,
|
pair,
|
||||||
@@ -1879,9 +1897,12 @@ class Exchange:
|
|||||||
orders = []
|
orders = []
|
||||||
if self.exchange_has("fetchClosedOrders"):
|
if self.exchange_has("fetchClosedOrders"):
|
||||||
orders = self._api.fetch_closed_orders(pair, since=since_ms)
|
orders = self._api.fetch_closed_orders(pair, since=since_ms)
|
||||||
if self.exchange_has("fetchOpenOrders"):
|
if self.exchange_has("fetchCanceledOrders"):
|
||||||
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
|
orders_canceled = self._api.fetch_canceled_orders(pair, since=since_ms)
|
||||||
orders.extend(orders_open)
|
orders.extend(orders_canceled)
|
||||||
|
if self.exchange_has("fetchOpenOrders"):
|
||||||
|
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
|
||||||
|
orders.extend(orders_open)
|
||||||
return orders
|
return orders
|
||||||
|
|
||||||
@retrier(retries=0)
|
@retrier(retries=0)
|
||||||
@@ -3914,7 +3935,6 @@ class Exchange:
|
|||||||
is_short: bool,
|
is_short: bool,
|
||||||
open_date: datetime,
|
open_date: datetime,
|
||||||
close_date: datetime,
|
close_date: datetime,
|
||||||
time_in_ratio: float | None = None,
|
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
calculates the sum of all funding fees that occurred for a pair during a futures trade
|
calculates the sum of all funding fees that occurred for a pair during a futures trade
|
||||||
@@ -3924,7 +3944,6 @@ class Exchange:
|
|||||||
:param is_short: trade direction
|
:param is_short: trade direction
|
||||||
:param open_date: The date and time that the trade started
|
:param open_date: The date and time that the trade started
|
||||||
:param close_date: The date and time that the trade ended
|
:param close_date: The date and time that the trade ended
|
||||||
:param time_in_ratio: Not used by most exchange classes
|
|
||||||
"""
|
"""
|
||||||
fees: float = 0
|
fees: float = 0
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,21 @@ def timeframe_to_msecs(timeframe: str) -> int:
|
|||||||
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
|
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
|
||||||
|
|
||||||
|
|
||||||
|
def timeframe_to_floor_freq(timeframe: str) -> str:
|
||||||
|
"""
|
||||||
|
Translates the timeframe interval value written in the human readable
|
||||||
|
form ('1m', '5m', '1h', '1d', '1w', etc.) to the desired floor frequency used by pandas
|
||||||
|
("1m", "5m", "1h", "1d", "1w", etc.).
|
||||||
|
Will use minute for most higher timeframes.
|
||||||
|
"""
|
||||||
|
timeframe_seconds = timeframe_to_seconds(timeframe)
|
||||||
|
timeframe_minutes = timeframe_seconds // 60
|
||||||
|
if timeframe_minutes <= 1:
|
||||||
|
return "1s"
|
||||||
|
else:
|
||||||
|
return "1min"
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_resample_freq(timeframe: str) -> str:
|
def timeframe_to_resample_freq(timeframe: str) -> str:
|
||||||
"""
|
"""
|
||||||
Translates the timeframe interval value written in the human readable
|
Translates the timeframe interval value written in the human readable
|
||||||
|
|||||||
@@ -5,11 +5,20 @@ from copy import deepcopy
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, TradingMode
|
||||||
from freqtrade.enums.runmode import NON_UTIL_MODES
|
from freqtrade.enums.runmode import NON_UTIL_MODES
|
||||||
from freqtrade.exceptions import ConfigurationError, ExchangeError, OperationalException
|
from freqtrade.exceptions import (
|
||||||
|
ConfigurationError,
|
||||||
|
DDosProtection,
|
||||||
|
ExchangeError,
|
||||||
|
OperationalException,
|
||||||
|
TemporaryError,
|
||||||
|
)
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.exchange.exchange_types import CcxtBalances, CcxtOrder, CcxtPosition, FtHas
|
from freqtrade.exchange.exchange_types import CcxtBalances, CcxtOrder, CcxtPosition, FtHas
|
||||||
from freqtrade.util.datetime_helpers import dt_from_ts
|
from freqtrade.util.datetime_helpers import dt_from_ts
|
||||||
|
|
||||||
@@ -22,6 +31,8 @@ class Hyperliquid(Exchange):
|
|||||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
unified_account = False
|
||||||
|
|
||||||
_ft_has: FtHas = {
|
_ft_has: FtHas = {
|
||||||
"ohlcv_has_history": False,
|
"ohlcv_has_history": False,
|
||||||
"l2_limit_range": [20],
|
"l2_limit_range": [20],
|
||||||
@@ -58,6 +69,38 @@ class Hyperliquid(Exchange):
|
|||||||
config.update(super()._ccxt_config)
|
config.update(super()._ccxt_config)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def additional_exchange_init(self) -> None:
|
||||||
|
"""
|
||||||
|
Additional exchange initialization logic.
|
||||||
|
.api will be available at this point.
|
||||||
|
Query User account Account Type to determine unified account status
|
||||||
|
https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#query-a-users-abstraction-state
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.trading_mode == TradingMode.FUTURES and not self._config["dry_run"]:
|
||||||
|
# Determine account status
|
||||||
|
# Unified accounts must use the spot endpoint for balances
|
||||||
|
request = {
|
||||||
|
"type": "userAbstraction",
|
||||||
|
"user": self._api.walletAddress,
|
||||||
|
}
|
||||||
|
response = self._api.publicPostInfo(request)
|
||||||
|
self.unified_account = response in ('"unifiedAccount"', '"portfolioMargin"')
|
||||||
|
if self.unified_account:
|
||||||
|
logger.info("Unified Hyperliquid account detected.")
|
||||||
|
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def _get_configured_hip3_dexes(self) -> list[str]:
|
def _get_configured_hip3_dexes(self) -> list[str]:
|
||||||
"""Get list of configured HIP-3 DEXes."""
|
"""Get list of configured HIP-3 DEXes."""
|
||||||
return self._config.get("exchange", {}).get("hip3_dexes", [])
|
return self._config.get("exchange", {}).get("hip3_dexes", [])
|
||||||
@@ -122,28 +165,33 @@ class Hyperliquid(Exchange):
|
|||||||
This override is not absolutely necessary and is only there for correct used / total values
|
This override is not absolutely necessary and is only there for correct used / total values
|
||||||
which are however not used by Freqtrade in futures mode at the moment.
|
which are however not used by Freqtrade in futures mode at the moment.
|
||||||
"""
|
"""
|
||||||
balances = super().get_balances()
|
params = params or {}
|
||||||
dexes = self._get_configured_hip3_dexes()
|
if self.unified_account:
|
||||||
for dex in dexes:
|
params["type"] = "spot"
|
||||||
try:
|
balances = super().get_balances(params)
|
||||||
dex_balance = super().get_balances(params={"dex": dex})
|
if not self.unified_account:
|
||||||
|
# In unified accounts, the balance already includes all DEXes
|
||||||
|
dexes = self._get_configured_hip3_dexes()
|
||||||
|
for dex in dexes:
|
||||||
|
try:
|
||||||
|
dex_balance = super().get_balances(params={"dex": dex})
|
||||||
|
|
||||||
for currency, amount_info in dex_balance.items():
|
for currency, amount_info in dex_balance.items():
|
||||||
if currency in ["info", "free", "used", "total", "datetime", "timestamp"]:
|
if currency in ["info", "free", "used", "total", "datetime", "timestamp"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if currency not in balances:
|
if currency not in balances:
|
||||||
balances[currency] = amount_info
|
balances[currency] = amount_info
|
||||||
else:
|
else:
|
||||||
balances[currency]["free"] += amount_info["free"]
|
balances[currency]["free"] += amount_info["free"]
|
||||||
balances[currency]["used"] += amount_info["used"]
|
balances[currency]["used"] += amount_info["used"]
|
||||||
balances[currency]["total"] += amount_info["total"]
|
balances[currency]["total"] += amount_info["total"]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Could not fetch balance for HIP-3 DEX '{dex}': {e}")
|
logger.error(f"Could not fetch balance for HIP-3 DEX '{dex}': {e}")
|
||||||
|
|
||||||
if dexes:
|
if dexes:
|
||||||
self._log_exchange_response("fetch_balance", balances, add_info="combined")
|
self._log_exchange_response("fetch_balance", balances, add_info="combined")
|
||||||
return balances
|
return balances
|
||||||
|
|
||||||
def fetch_positions(
|
def fetch_positions(
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
"""Kraken exchange subclass"""
|
"""Kraken exchange subclass"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
from pandas import DataFrame
|
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, TradingMode
|
||||||
@@ -40,7 +38,6 @@ class Kraken(Exchange):
|
|||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
(TradingMode.SPOT, MarginMode.NONE),
|
(TradingMode.SPOT, MarginMode.NONE),
|
||||||
# (TradingMode.MARGIN, MarginMode.CROSS),
|
# (TradingMode.MARGIN, MarginMode.CROSS),
|
||||||
# (TradingMode.FUTURES, MarginMode.CROSS)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def market_is_tradable(self, market: dict[str, Any]) -> bool:
|
def market_is_tradable(self, market: dict[str, Any]) -> bool:
|
||||||
@@ -114,18 +111,6 @@ class Kraken(Exchange):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def _set_leverage(
|
|
||||||
self,
|
|
||||||
leverage: float,
|
|
||||||
pair: str | None = None,
|
|
||||||
accept_fail: bool = False,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Kraken set's the leverage as an option in the order object, so we need to
|
|
||||||
add it to params
|
|
||||||
"""
|
|
||||||
return
|
|
||||||
|
|
||||||
def _get_params(
|
def _get_params(
|
||||||
self,
|
self,
|
||||||
side: BuySell,
|
side: BuySell,
|
||||||
@@ -148,41 +133,6 @@ class Kraken(Exchange):
|
|||||||
params["postOnly"] = True
|
params["postOnly"] = True
|
||||||
return params
|
return params
|
||||||
|
|
||||||
def calculate_funding_fees(
|
|
||||||
self,
|
|
||||||
df: DataFrame,
|
|
||||||
amount: float,
|
|
||||||
is_short: bool,
|
|
||||||
open_date: datetime,
|
|
||||||
close_date: datetime,
|
|
||||||
time_in_ratio: float | None = None,
|
|
||||||
) -> float:
|
|
||||||
"""
|
|
||||||
# ! This method will always error when run by Freqtrade because time_in_ratio is never
|
|
||||||
# ! passed to _get_funding_fee. For kraken futures to work in dry run and backtesting
|
|
||||||
# ! functionality must be added that passes the parameter time_in_ratio to
|
|
||||||
# ! _get_funding_fee when using Kraken
|
|
||||||
calculates the sum of all funding fees that occurred for a pair during a futures trade
|
|
||||||
:param df: Dataframe containing combined funding and mark rates
|
|
||||||
as `open_fund` and `open_mark`.
|
|
||||||
:param amount: The quantity of the trade
|
|
||||||
:param is_short: trade direction
|
|
||||||
:param open_date: The date and time that the trade started
|
|
||||||
:param close_date: The date and time that the trade ended
|
|
||||||
:param time_in_ratio: Not used by most exchange classes
|
|
||||||
"""
|
|
||||||
if not time_in_ratio:
|
|
||||||
raise OperationalException(
|
|
||||||
f"time_in_ratio is required for {self.name}._get_funding_fee"
|
|
||||||
)
|
|
||||||
fees: float = 0
|
|
||||||
|
|
||||||
if not df.empty:
|
|
||||||
df = df[(df["date"] >= open_date) & (df["date"] <= close_date)]
|
|
||||||
fees = sum(df["open_fund"] * df["open_mark"] * amount * time_in_ratio)
|
|
||||||
|
|
||||||
return fees if is_short else -fees
|
|
||||||
|
|
||||||
def _get_trade_pagination_next_value(self, trades: list[dict]):
|
def _get_trade_pagination_next_value(self, trades: list[dict]):
|
||||||
"""
|
"""
|
||||||
Extract pagination id for the next "from_id" value
|
Extract pagination id for the next "from_id" value
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
"""Kraken Futures exchange subclass"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
|
||||||
|
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||||
|
from freqtrade.exceptions import (
|
||||||
|
DDosProtection,
|
||||||
|
ExchangeError,
|
||||||
|
InvalidOrderException,
|
||||||
|
OperationalException,
|
||||||
|
TemporaryError,
|
||||||
|
)
|
||||||
|
from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier
|
||||||
|
from freqtrade.exchange.exchange import Exchange
|
||||||
|
from freqtrade.exchange.exchange_types import CcxtBalances, CcxtOrder, FtHas
|
||||||
|
from freqtrade.misc import safe_value_nested
|
||||||
|
from freqtrade.util.datetime_helpers import dt_from_ts
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Krakenfutures(Exchange):
|
||||||
|
"""Kraken Futures exchange class.
|
||||||
|
|
||||||
|
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||||
|
|
||||||
|
Key differences from spot Kraken:
|
||||||
|
- Stop orders use triggerPrice/triggerSignal instead of stopPrice
|
||||||
|
- Flex (multi-collateral) accounts need USD balance synthesis
|
||||||
|
"""
|
||||||
|
|
||||||
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
|
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||||
|
]
|
||||||
|
|
||||||
|
_ft_has: FtHas = {
|
||||||
|
"tickers_have_quoteVolume": False,
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {
|
||||||
|
"limit": "limit",
|
||||||
|
"market": "market",
|
||||||
|
},
|
||||||
|
"stoploss_query_requires_stop_flag": True,
|
||||||
|
"stop_price_param": "triggerPrice",
|
||||||
|
"stop_price_prop": "stopPrice",
|
||||||
|
"stop_price_type_field": "triggerSignal",
|
||||||
|
"stop_price_type_value_mapping": {
|
||||||
|
PriceType.LAST: "last",
|
||||||
|
PriceType.MARK: "mark",
|
||||||
|
PriceType.INDEX: "index",
|
||||||
|
},
|
||||||
|
"exchange_has_overrides": {"fetchOrders": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||||
|
"""
|
||||||
|
Fetch balances with USD synthesis for flex (multi-collateral) accounts.
|
||||||
|
|
||||||
|
Kraken Futures flex accounts hold multiple currencies as collateral.
|
||||||
|
CCXT returns per-currency balances but doesn't expose margin values
|
||||||
|
as a USD balance. This override synthesizes a USD entry from flex account data
|
||||||
|
when stake_currency is USD.
|
||||||
|
|
||||||
|
Field mapping (margin-centric for internal consistency):
|
||||||
|
- free: availableMargin (margin available for new positions)
|
||||||
|
- total: marginEquity (haircut-adjusted collateral + unrealized P&L)
|
||||||
|
- used: total - free (margin currently in use)
|
||||||
|
|
||||||
|
Fallback chain for total: marginEquity -> portfolioValue -> balanceValue
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
balances = self._api.fetch_balance(params or {})
|
||||||
|
|
||||||
|
# Only synthesize USD if stake_currency is USD
|
||||||
|
stake = str(self._config.get("stake_currency", "")).upper()
|
||||||
|
if stake == "USD":
|
||||||
|
# Only synthesize if USD stake - flex only applies for these currencies.
|
||||||
|
# For flex accounts, synthesize USD balance from margin values
|
||||||
|
info = balances.get("info", {})
|
||||||
|
accounts = info.get("accounts", {}) if isinstance(info, dict) else {}
|
||||||
|
flex = accounts.get("flex", {}) if isinstance(accounts, dict) else {}
|
||||||
|
|
||||||
|
if flex:
|
||||||
|
usd_free = self._safe_float(flex.get("availableMargin"))
|
||||||
|
# Prefer marginEquity for consistency (same basis as availableMargin)
|
||||||
|
raw_total = (
|
||||||
|
flex.get("marginEquity")
|
||||||
|
or flex.get("portfolioValue")
|
||||||
|
or flex.get("balanceValue")
|
||||||
|
)
|
||||||
|
usd_total = self._safe_float(raw_total)
|
||||||
|
if usd_free is not None or usd_total is not None:
|
||||||
|
# Use available value for both if only one is present
|
||||||
|
usd_free_value = usd_free if usd_free is not None else usd_total
|
||||||
|
usd_total_value = usd_total if usd_total is not None else usd_free
|
||||||
|
if usd_free_value is not None and usd_total_value is not None:
|
||||||
|
usd_used = max(0.0, usd_total_value - usd_free_value)
|
||||||
|
balances["USD"] = {
|
||||||
|
"free": usd_free_value,
|
||||||
|
"used": usd_used,
|
||||||
|
"total": usd_total_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove additional info from ccxt results (same as base class)
|
||||||
|
balances.pop("info", None)
|
||||||
|
balances.pop("free", None)
|
||||||
|
balances.pop("total", None)
|
||||||
|
balances.pop("used", None)
|
||||||
|
|
||||||
|
self._log_exchange_response("fetch_balance", balances, add_info=params)
|
||||||
|
return balances
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f"Could not get balance due to {e.__class__.__name__}. Message: {e}"
|
||||||
|
) from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _safe_float(value: Any) -> float | None:
|
||||||
|
"""Convert value to float, returning None if conversion fails."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _order_contracts_to_amount(self, order: CcxtOrder) -> CcxtOrder:
|
||||||
|
"""Normalize order and apply Kraken Futures-specific order corrections."""
|
||||||
|
order = super()._order_contracts_to_amount(order)
|
||||||
|
return self._adjust_krakenfutures_order(order)
|
||||||
|
|
||||||
|
def _adjust_krakenfutures_order(self, order: CcxtOrder) -> CcxtOrder:
|
||||||
|
"""Apply Kraken Futures-specific order corrections.
|
||||||
|
|
||||||
|
For filled terminal orders, always fetch trades and compute VWAP because
|
||||||
|
CCXT's average is still unreliable.
|
||||||
|
|
||||||
|
See: https://github.com/ccxt/ccxt/issues/27996
|
||||||
|
"""
|
||||||
|
if order.get("status") == "canceled" and order.get("filled") is None:
|
||||||
|
# Workaround for missing filled parsing - https://github.com/ccxt/ccxt/issues/28210
|
||||||
|
order["filled"] = safe_value_nested(order, "info.order.filled", default_value=None)
|
||||||
|
|
||||||
|
filled = self._safe_float(order.get("filled")) or 0.0
|
||||||
|
if order.get("status") in ("canceled", "closed") and filled > 0:
|
||||||
|
# Compute VWAP and cost for filled orders.
|
||||||
|
trades = self.get_trades_for_order(
|
||||||
|
order["id"], order["symbol"], since=dt_from_ts(order["timestamp"])
|
||||||
|
)
|
||||||
|
if trades:
|
||||||
|
total_amount = sum(t["amount"] for t in trades)
|
||||||
|
if total_amount:
|
||||||
|
# Compute VWAP
|
||||||
|
order["average"] = sum(t["price"] * t["amount"] for t in trades) / total_amount
|
||||||
|
trade_costs = [t["cost"] for t in trades if t.get("cost") is not None]
|
||||||
|
if trade_costs:
|
||||||
|
order["cost"] = sum(trade_costs)
|
||||||
|
return order
|
||||||
|
|
||||||
|
def get_trades_for_order(
|
||||||
|
self, order_id: str, pair: str, since: datetime, params: dict | None = None
|
||||||
|
) -> list:
|
||||||
|
"""Fetch trades and enrich with calculated fees.
|
||||||
|
|
||||||
|
Kraken Futures' /fills endpoint does not include fee amounts — only
|
||||||
|
fillType (maker/taker). This enriches each trade with a calculated fee
|
||||||
|
using the market's fee schedule so Freqtrade's fee detection works.
|
||||||
|
"""
|
||||||
|
trades = super().get_trades_for_order(order_id, pair, since, params)
|
||||||
|
for trade in trades:
|
||||||
|
if trade.get("fee") is None or trade["fee"].get("cost") is None:
|
||||||
|
taker_or_maker = trade.get("takerOrMaker", "taker")
|
||||||
|
symbol = trade.get("symbol", pair)
|
||||||
|
market = self.markets.get(symbol, {})
|
||||||
|
fee_rate = market.get(taker_or_maker, market.get("taker", 0.0005))
|
||||||
|
cost = trade.get("cost")
|
||||||
|
if cost is not None and fee_rate is not None:
|
||||||
|
trade["fee"] = {
|
||||||
|
"cost": cost * fee_rate,
|
||||||
|
"currency": market.get("quote", "USD"),
|
||||||
|
"rate": fee_rate,
|
||||||
|
}
|
||||||
|
return trades
|
||||||
|
|
||||||
|
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||||
|
def fetch_order(
|
||||||
|
self, order_id: str, pair: str, params: dict[str, Any] | None = None
|
||||||
|
) -> CcxtOrder:
|
||||||
|
"""Fetch order with direct CCXT call and fallback to history endpoints."""
|
||||||
|
if self._config.get("dry_run"):
|
||||||
|
return self.fetch_dry_run_order(order_id)
|
||||||
|
|
||||||
|
params = params or {}
|
||||||
|
status_params = {k: v for k, v in params.items() if k not in ("trigger", "stop")}
|
||||||
|
try:
|
||||||
|
order = self._api.fetch_order(order_id, pair, params=status_params)
|
||||||
|
self._log_exchange_response("fetch_order", order)
|
||||||
|
return self._order_contracts_to_amount(order)
|
||||||
|
except ccxt.OrderNotFound:
|
||||||
|
# Expected for older Kraken Futures orders not visible in orders/status.
|
||||||
|
pass
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
msg = f"Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}"
|
||||||
|
raise InvalidOrderException(msg) from e
|
||||||
|
except (ccxt.OperationFailed, ccxt.ExchangeError):
|
||||||
|
# Fallback to history endpoints for temporary/status endpoint gaps.
|
||||||
|
pass
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
order = self._fetch_order_fallback(order_id, pair, params)
|
||||||
|
if order is not None:
|
||||||
|
return order
|
||||||
|
|
||||||
|
# Order not in status, open, closed, or canceled endpoints - genuinely gone.
|
||||||
|
# Raise non-retrying InvalidOrderException (Kraken has limited history retention).
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f"Order not found in any endpoint (pair: {pair} id: {order_id})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fetch_order_fallback(
|
||||||
|
self, order_id: str, pair: str, params: dict[str, Any]
|
||||||
|
) -> CcxtOrder | None:
|
||||||
|
"""Search open, closed, and canceled order endpoints for order_id.
|
||||||
|
|
||||||
|
Kraken Futures' orders/status endpoint only returns currently open orders.
|
||||||
|
Older orders require querying history endpoints (closed/canceled).
|
||||||
|
For stoploss (trigger) orders, the caller should pass stop=True in params
|
||||||
|
(handled automatically via stoploss_query_requires_stop_flag in _ft_has)
|
||||||
|
so that closed/canceled queries hit the trigger history endpoint.
|
||||||
|
"""
|
||||||
|
order_id_str = str(order_id)
|
||||||
|
|
||||||
|
# Open orders include triggers by default. Avoid passing trigger/stop flags
|
||||||
|
# to prevent endpoint/filter mismatches.
|
||||||
|
open_params = {k: v for k, v in params.items() if k not in ("trigger", "stop")}
|
||||||
|
order = self._find_order_in_list(
|
||||||
|
self._api.fetch_open_orders, pair, open_params, order_id_str
|
||||||
|
)
|
||||||
|
if order is not None:
|
||||||
|
return order
|
||||||
|
|
||||||
|
# Closed/canceled: pass params through (including stop=True for stoploss orders,
|
||||||
|
# which CCXT maps to the trigger history endpoint).
|
||||||
|
for fetch_fn in (self._api.fetch_closed_orders, self._api.fetch_canceled_orders):
|
||||||
|
order = self._find_order_in_list(fetch_fn, pair, params, order_id_str)
|
||||||
|
if order is not None:
|
||||||
|
return order
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _find_order_in_list(
|
||||||
|
self,
|
||||||
|
fetch_fn,
|
||||||
|
symbol: str | None,
|
||||||
|
params: dict[str, Any],
|
||||||
|
order_id_str: str,
|
||||||
|
) -> CcxtOrder | None:
|
||||||
|
"""Fetch orders and return matching order_id, or None."""
|
||||||
|
try:
|
||||||
|
orders = fetch_fn(symbol, params=params) or []
|
||||||
|
self._log_exchange_response(fetch_fn.__name__, orders)
|
||||||
|
for order in orders:
|
||||||
|
if str(order.get("id")) == order_id_str:
|
||||||
|
self._log_exchange_response("fetch_order_fallback", order)
|
||||||
|
|
||||||
|
return self._order_contracts_to_amount(order)
|
||||||
|
except (ccxt.OrderNotFound, ccxt.InvalidOrder) as e:
|
||||||
|
logger.debug(f"{fetch_fn.__name__} failed: {e}")
|
||||||
|
return None
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
|
||||||
|
) from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_funding_fees(self, pair: str, amount: float, is_short: bool, open_date) -> float:
|
||||||
|
"""Fetch funding fees, returning 0.0 if retrieval fails."""
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
try:
|
||||||
|
return self._fetch_and_calculate_funding_fees(pair, amount, is_short, open_date)
|
||||||
|
except ExchangeError:
|
||||||
|
logger.warning(f"Could not update funding fees for {pair}.")
|
||||||
|
return 0.0
|
||||||
@@ -446,7 +446,7 @@ class FreqaiDataDrawer:
|
|||||||
|
|
||||||
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
|
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
|
||||||
|
|
||||||
pattern = re.compile(r"sub-train-(\w+)_(\d{10})")
|
pattern = re.compile(r"^sub-train-(.+)_(\d{10})$")
|
||||||
|
|
||||||
delete_dict: dict[str, Any] = {}
|
delete_dict: dict[str, Any] = {}
|
||||||
|
|
||||||
@@ -614,9 +614,13 @@ class FreqaiDataDrawer:
|
|||||||
elif self.model_type == "pytorch":
|
elif self.model_type == "pytorch":
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
zipfile = torch.load(dk.data_path / f"{dk.model_filename}_model.zip")
|
zipfile = torch.load(
|
||||||
model = zipfile["pytrainer"]
|
dk.data_path / f"{dk.model_filename}_model.zip",
|
||||||
model = model.load_from_checkpoint(zipfile)
|
weights_only=False,
|
||||||
|
)
|
||||||
|
# weights_only is necessary due to pytrainer being a serialized python object.
|
||||||
|
_trainer = zipfile["pytrainer"]
|
||||||
|
model = _trainer.load_from_checkpoint(zipfile)
|
||||||
|
|
||||||
if not model:
|
if not model:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
|||||||
@@ -428,22 +428,28 @@ class FreqaiDataKitchen:
|
|||||||
Get backtest prediction from current backtest period
|
Get backtest prediction from current backtest period
|
||||||
"""
|
"""
|
||||||
|
|
||||||
append_df = DataFrame()
|
# Build dict first and construct DataFrame once to avoid
|
||||||
|
# column-by-column assignment which causes DataFrame fragmentation
|
||||||
|
# and PerformanceWarning on large prediction sets.
|
||||||
|
append_dict: dict[str, Any] = {}
|
||||||
|
|
||||||
for label in predictions.columns:
|
for label in predictions.columns:
|
||||||
append_df[label] = predictions[label]
|
append_dict[label] = predictions[label]
|
||||||
if append_df[label].dtype == object:
|
if predictions[label].dtype == object:
|
||||||
continue
|
continue
|
||||||
if "labels_mean" in self.data:
|
if "labels_mean" in self.data and label in self.data["labels_mean"]:
|
||||||
append_df[f"{label}_mean"] = self.data["labels_mean"][label]
|
append_dict[f"{label}_mean"] = self.data["labels_mean"][label]
|
||||||
if "labels_std" in self.data:
|
if "labels_std" in self.data and label in self.data["labels_std"]:
|
||||||
append_df[f"{label}_std"] = self.data["labels_std"][label]
|
append_dict[f"{label}_std"] = self.data["labels_std"][label]
|
||||||
|
|
||||||
for extra_col in self.data["extra_returns_per_train"]:
|
for extra_col in self.data["extra_returns_per_train"]:
|
||||||
append_df[f"{extra_col}"] = self.data["extra_returns_per_train"][extra_col]
|
append_dict[f"{extra_col}"] = self.data["extra_returns_per_train"][extra_col]
|
||||||
|
|
||||||
append_df["do_predict"] = do_predict
|
append_dict["do_predict"] = do_predict
|
||||||
if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0:
|
if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0:
|
||||||
append_df["DI_values"] = self.DI_values
|
append_dict["DI_values"] = self.DI_values
|
||||||
|
|
||||||
|
append_df = DataFrame(append_dict)
|
||||||
|
|
||||||
user_cols = [col for col in dataframe_backtest.columns if col.startswith("%%")]
|
user_cols = [col for col in dataframe_backtest.columns if col.startswith("%%")]
|
||||||
cols = ["date"]
|
cols = ["date"]
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||||||
self.tb_logger = tb_logger
|
self.tb_logger = tb_logger
|
||||||
self.test_batch_counter = 0
|
self.test_batch_counter = 0
|
||||||
|
|
||||||
|
# Early stopping parameters
|
||||||
|
self.early_stopping_patience: int = kwargs.get("early_stopping_patience", 0)
|
||||||
|
self.best_val_loss: float = float("inf")
|
||||||
|
self.patience_counter: int = 0
|
||||||
|
|
||||||
def fit(self, data_dictionary: dict[str, pd.DataFrame], splits: list[str]):
|
def fit(self, data_dictionary: dict[str, pd.DataFrame], splits: list[str]):
|
||||||
"""
|
"""
|
||||||
:param data_dictionary: the dictionary constructed by DataHandler to hold
|
:param data_dictionary: the dictionary constructed by DataHandler to hold
|
||||||
@@ -99,15 +104,40 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||||||
|
|
||||||
# evaluation
|
# evaluation
|
||||||
if "test" in splits:
|
if "test" in splits:
|
||||||
self.estimate_loss(data_loaders_dictionary, "test")
|
val_loss = self.estimate_loss(data_loaders_dictionary, "test")
|
||||||
|
|
||||||
|
# Early stopping check
|
||||||
|
if self.early_stopping_patience > 0 and val_loss is not None:
|
||||||
|
if val_loss < self.best_val_loss:
|
||||||
|
self.best_val_loss = val_loss
|
||||||
|
self.patience_counter = 0
|
||||||
|
else:
|
||||||
|
self.patience_counter += 1
|
||||||
|
if self.patience_counter >= self.early_stopping_patience:
|
||||||
|
logger.info(
|
||||||
|
f"Early stopping triggered after {self.patience_counter} "
|
||||||
|
f"epochs without improvement. "
|
||||||
|
f"Best val_loss: {self.best_val_loss:.6f}"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
@torch.no_grad()
|
@torch.no_grad()
|
||||||
def estimate_loss(
|
def estimate_loss(
|
||||||
self,
|
self,
|
||||||
data_loader_dictionary: dict[str, DataLoader],
|
data_loader_dictionary: dict[str, DataLoader],
|
||||||
split: str,
|
split: str,
|
||||||
) -> None:
|
) -> float | None:
|
||||||
|
"""
|
||||||
|
Estimate loss on a data split.
|
||||||
|
|
||||||
|
:param data_loader_dictionary: dictionary of data loaders.
|
||||||
|
:param split: split to estimate loss on (e.g. "test").
|
||||||
|
:return: average loss over all batches, or None if no batches.
|
||||||
|
"""
|
||||||
self.model.eval()
|
self.model.eval()
|
||||||
|
total_loss = 0.0
|
||||||
|
num_batches = 0
|
||||||
|
|
||||||
for _, batch_data in enumerate(data_loader_dictionary[split]):
|
for _, batch_data in enumerate(data_loader_dictionary[split]):
|
||||||
xb, yb = batch_data
|
xb, yb = batch_data
|
||||||
xb = xb.to(self.device)
|
xb = xb.to(self.device)
|
||||||
@@ -115,11 +145,17 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||||||
|
|
||||||
yb_pred = self.model(xb)
|
yb_pred = self.model(xb)
|
||||||
loss = self.criterion(yb_pred, yb)
|
loss = self.criterion(yb_pred, yb)
|
||||||
|
total_loss += loss.item()
|
||||||
|
num_batches += 1
|
||||||
self.tb_logger.log_scalar(f"{split}_loss", loss.item(), self.test_batch_counter)
|
self.tb_logger.log_scalar(f"{split}_loss", loss.item(), self.test_batch_counter)
|
||||||
self.test_batch_counter += 1
|
self.test_batch_counter += 1
|
||||||
|
|
||||||
self.model.train()
|
self.model.train()
|
||||||
|
|
||||||
|
if num_batches > 0:
|
||||||
|
return total_loss / num_batches
|
||||||
|
return None
|
||||||
|
|
||||||
def create_data_loaders_dictionary(
|
def create_data_loaders_dictionary(
|
||||||
self, data_dictionary: dict[str, pd.DataFrame], splits: list[str]
|
self, data_dictionary: dict[str, pd.DataFrame], splits: list[str]
|
||||||
) -> dict[str, DataLoader]:
|
) -> dict[str, DataLoader]:
|
||||||
@@ -179,16 +215,12 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||||||
path,
|
path,
|
||||||
)
|
)
|
||||||
|
|
||||||
def load(self, path: Path):
|
|
||||||
checkpoint = torch.load(path)
|
|
||||||
return self.load_from_checkpoint(checkpoint)
|
|
||||||
|
|
||||||
def load_from_checkpoint(self, checkpoint: dict):
|
def load_from_checkpoint(self, checkpoint: dict):
|
||||||
"""
|
"""
|
||||||
when using continual_learning, DataDrawer will load the dictionary
|
when using continual_learning, DataDrawer will load the dictionary
|
||||||
(containing state dicts and model_meta_data) by calling torch.load(path).
|
(containing state dicts and model_meta_data) by calling torch.load(path).
|
||||||
you can access this dict from any class that inherits IFreqaiModel by calling
|
you can access this dict from any class that inherits IFreqaiModel by calling
|
||||||
get_init_model method.
|
the get_init_model method.
|
||||||
"""
|
"""
|
||||||
self.model.load_state_dict(checkpoint["model_state_dict"])
|
self.model.load_state_dict(checkpoint["model_state_dict"])
|
||||||
self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
|
self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
|
||||||
|
|||||||
@@ -555,7 +555,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if trade.base_currency
|
if trade.base_currency
|
||||||
else 0
|
else 0
|
||||||
)
|
)
|
||||||
if total < trade.amount:
|
if total < trade.amount or (total == 0 and trade.amount == 0):
|
||||||
if trade.fully_canceled_entry_order_count == len(trade.orders):
|
if trade.fully_canceled_entry_order_count == len(trade.orders):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Trade only had fully canceled entry orders. "
|
f"Trade only had fully canceled entry orders. "
|
||||||
@@ -2421,7 +2421,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
def handle_protections(self, pair: str, side: LongShort) -> None:
|
def handle_protections(self, pair: str, side: LongShort) -> None:
|
||||||
# Lock pair for one candle to prevent immediate re-entries
|
# Lock pair for one candle to prevent immediate re-entries
|
||||||
self.strategy.lock_pair(pair, datetime.now(UTC), reason="Auto lock", side=side)
|
self.strategy.lock_pair(pair, datetime.now(UTC), reason="Auto lock", side=side)
|
||||||
prot_trig = self.protections.stop_per_pair(pair, side=side)
|
starting_balance = self.wallets.get_starting_balance()
|
||||||
|
prot_trig = self.protections.stop_per_pair(
|
||||||
|
pair, side=side, starting_balance=starting_balance
|
||||||
|
)
|
||||||
if prot_trig:
|
if prot_trig:
|
||||||
msg: RPCProtectionMsg = {
|
msg: RPCProtectionMsg = {
|
||||||
"type": RPCMessageType.PROTECTION_TRIGGER,
|
"type": RPCMessageType.PROTECTION_TRIGGER,
|
||||||
@@ -2430,7 +2433,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
}
|
}
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
prot_trig_glb = self.protections.global_stop(side=side)
|
prot_trig_glb = self.protections.global_stop(side=side, starting_balance=starting_balance)
|
||||||
if prot_trig_glb:
|
if prot_trig_glb:
|
||||||
msg = {
|
msg = {
|
||||||
"type": RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
|
"type": RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class PointAnnotationType(_BaseAnnotationType, total=False):
|
|||||||
y: float
|
y: float
|
||||||
size: int
|
size: int
|
||||||
shape: Literal["circle", "rect", "roundRect", "triangle", "pin", "arrow", "none"]
|
shape: Literal["circle", "rect", "roundRect", "triangle", "pin", "arrow", "none"]
|
||||||
|
rotate: int
|
||||||
|
|
||||||
|
|
||||||
AnnotationType = AreaAnnotationType | LineAnnotationType | PointAnnotationType
|
AnnotationType = AreaAnnotationType | LineAnnotationType | PointAnnotationType
|
||||||
|
|||||||
+3
-5
@@ -6,7 +6,6 @@ Read the documentation to know what cli arguments you need.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
# check min. python version
|
# check min. python version
|
||||||
@@ -35,7 +34,7 @@ def main(sysargv: list[str] | None = None) -> None:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return_code: Any = 1
|
return_code: int | None = None
|
||||||
try:
|
try:
|
||||||
setup_logging_pre()
|
setup_logging_pre()
|
||||||
asyncio_setup()
|
asyncio_setup()
|
||||||
@@ -62,11 +61,9 @@ def main(sysargv: list[str] | None = None) -> None:
|
|||||||
"`freqtrade --help` or `freqtrade <command> --help`."
|
"`freqtrade --help` or `freqtrade <command> --help`."
|
||||||
)
|
)
|
||||||
|
|
||||||
except SystemExit as e: # pragma: no cover
|
|
||||||
return_code = e
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info("SIGINT received, aborting ...")
|
logger.info("SIGINT received, aborting ...")
|
||||||
return_code = 0
|
return_code = 130
|
||||||
except ConfigurationError as e:
|
except ConfigurationError as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Configuration error: {e}\n"
|
f"Configuration error: {e}\n"
|
||||||
@@ -77,6 +74,7 @@ def main(sysargv: list[str] | None = None) -> None:
|
|||||||
return_code = 2
|
return_code = 2
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Fatal exception!")
|
logger.exception("Fatal exception!")
|
||||||
|
return_code = 1
|
||||||
finally:
|
finally:
|
||||||
sys.exit(return_code)
|
sys.exit(return_code)
|
||||||
|
|
||||||
|
|||||||
+33
-7
@@ -84,7 +84,12 @@ def file_load_json(file: Path):
|
|||||||
|
|
||||||
def is_file_in_dir(file: Path, directory: Path) -> bool:
|
def is_file_in_dir(file: Path, directory: Path) -> bool:
|
||||||
"""
|
"""
|
||||||
Helper function to check if file is in directory.
|
Helper function to check if file is directly within a directory.
|
||||||
|
:param file: File to check
|
||||||
|
:param directory: Directory to check against
|
||||||
|
When used in the API, this parameter cannot be user controlled (outside of the config)
|
||||||
|
to avoid security issues.
|
||||||
|
:return: True if file is directly within directory, False otherwise
|
||||||
"""
|
"""
|
||||||
return file.is_file() and file.parent.samefile(directory)
|
return file.is_file() and file.parent.samefile(directory)
|
||||||
|
|
||||||
@@ -125,6 +130,27 @@ def round_dict(d, n):
|
|||||||
DictMap = dict[str, Any] | Mapping[str, Any]
|
DictMap = dict[str, Any] | Mapping[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def safe_value_nested(obj: DictMap, keys: str, default_value=None):
|
||||||
|
"""
|
||||||
|
Search a nested dict for a value.
|
||||||
|
:param obj: dict to search in
|
||||||
|
:param keys: dot separated keys to search for
|
||||||
|
:param default_value: value to return if the key is not found or value is None
|
||||||
|
:return: value found in dict or default_value
|
||||||
|
Sample:
|
||||||
|
>>> d = { 'first' : { 'rows' : { 'pass' : 'dog', 'number' : '1' } } }
|
||||||
|
>>> safe_value_nested(d, "first.rows.pass") == "dog"
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
nested_obj = obj
|
||||||
|
for key in keys.split("."):
|
||||||
|
if isinstance(nested_obj, Mapping) and key in nested_obj and nested_obj[key] is not None:
|
||||||
|
nested_obj = nested_obj[key]
|
||||||
|
else:
|
||||||
|
return default_value
|
||||||
|
return nested_obj
|
||||||
|
|
||||||
|
|
||||||
def safe_value_fallback(obj: DictMap, key1: str, key2: str | None = None, default_value=None):
|
def safe_value_fallback(obj: DictMap, key1: str, key2: str | None = None, default_value=None):
|
||||||
"""
|
"""
|
||||||
Search a value in obj, return this if it's not None.
|
Search a value in obj, return this if it's not None.
|
||||||
@@ -210,12 +236,12 @@ def remove_entry_exit_signals(dataframe: pd.DataFrame):
|
|||||||
|
|
||||||
:param dataframe: The DataFrame to remove signals from
|
:param dataframe: The DataFrame to remove signals from
|
||||||
"""
|
"""
|
||||||
dataframe[SignalType.ENTER_LONG.value] = 0
|
dataframe[SignalType.ENTER_LONG] = 0
|
||||||
dataframe[SignalType.EXIT_LONG.value] = 0
|
dataframe[SignalType.EXIT_LONG] = 0
|
||||||
dataframe[SignalType.ENTER_SHORT.value] = 0
|
dataframe[SignalType.ENTER_SHORT] = 0
|
||||||
dataframe[SignalType.EXIT_SHORT.value] = 0
|
dataframe[SignalType.EXIT_SHORT] = 0
|
||||||
dataframe[SignalTagType.ENTER_TAG.value] = None
|
dataframe[SignalTagType.ENTER_TAG] = None
|
||||||
dataframe[SignalTagType.EXIT_TAG.value] = None
|
dataframe[SignalTagType.EXIT_TAG] = None
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ class Backtesting:
|
|||||||
"exited": {},
|
"exited": {},
|
||||||
}
|
}
|
||||||
self.rejected_dict: dict[str, list] = {}
|
self.rejected_dict: dict[str, list] = {}
|
||||||
|
self.starting_balance: float = 0.0
|
||||||
|
|
||||||
self._exchange_name = self.config["exchange"]["name"]
|
self._exchange_name = self.config["exchange"]["name"]
|
||||||
self.__initial_backtest = exchange is None
|
self.__initial_backtest = exchange is None
|
||||||
@@ -277,6 +278,7 @@ class Backtesting:
|
|||||||
self.reset_backtest(False)
|
self.reset_backtest(False)
|
||||||
|
|
||||||
self.wallets = Wallets(self.config, self.exchange, is_backtest=True)
|
self.wallets = Wallets(self.config, self.exchange, is_backtest=True)
|
||||||
|
self.starting_balance = self.wallets.get_starting_balance()
|
||||||
|
|
||||||
self.progress = BTProgress()
|
self.progress = BTProgress()
|
||||||
self.abort = False
|
self.abort = False
|
||||||
@@ -846,9 +848,7 @@ class Backtesting:
|
|||||||
exit_tag=exit_reason,
|
exit_tag=exit_reason,
|
||||||
)
|
)
|
||||||
if rate is not None and rate != close_rate:
|
if rate is not None and rate != close_rate:
|
||||||
close_rate = price_to_precision(
|
close_rate = rate
|
||||||
rate, trade.price_precision, trade.precision_mode_price
|
|
||||||
)
|
|
||||||
# We can't place orders lower than current low.
|
# We can't place orders lower than current low.
|
||||||
# freqtrade does not support this in live, and the order would fill immediately
|
# freqtrade does not support this in live, and the order would fill immediately
|
||||||
if trade.is_short:
|
if trade.is_short:
|
||||||
@@ -890,6 +890,9 @@ class Backtesting:
|
|||||||
self.order_id_counter += 1
|
self.order_id_counter += 1
|
||||||
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||||
order_type = self.strategy.order_types["exit"]
|
order_type = self.strategy.order_types["exit"]
|
||||||
|
close_rate = price_to_precision(
|
||||||
|
close_rate, trade.price_precision, trade.precision_mode_price
|
||||||
|
)
|
||||||
# amount = amount or trade.amount
|
# amount = amount or trade.amount
|
||||||
amount = amount_to_contract_precision(
|
amount = amount_to_contract_precision(
|
||||||
amount or trade.amount, trade.amount_precision, self.precision_mode, trade.contract_size
|
amount or trade.amount, trade.amount_precision, self.precision_mode, trade.contract_size
|
||||||
@@ -1271,8 +1274,8 @@ class Backtesting:
|
|||||||
|
|
||||||
def run_protections(self, pair: str, current_time: datetime, side: LongShort):
|
def run_protections(self, pair: str, current_time: datetime, side: LongShort):
|
||||||
if self.enable_protections:
|
if self.enable_protections:
|
||||||
self.protections.stop_per_pair(pair, current_time, side)
|
self.protections.stop_per_pair(pair, current_time, side, self.starting_balance)
|
||||||
self.protections.global_stop(current_time, side)
|
self.protections.global_stop(current_time, side, self.starting_balance)
|
||||||
|
|
||||||
def manage_open_orders(self, trade: LocalTrade, current_time: datetime, row: tuple) -> bool:
|
def manage_open_orders(self, trade: LocalTrade, current_time: datetime, row: tuple) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
from typing import ClassVar, Literal
|
from typing import ClassVar, Literal
|
||||||
|
|
||||||
from sqlalchemy import String
|
from sqlalchemy import String
|
||||||
@@ -11,7 +11,7 @@ from freqtrade.persistence.base import ModelBase, SessionType
|
|||||||
ValueTypes = str | datetime | float | int
|
ValueTypes = str | datetime | float | int
|
||||||
|
|
||||||
|
|
||||||
class ValueTypesEnum(str, Enum):
|
class ValueTypesEnum(StrEnum):
|
||||||
STRING = "str"
|
STRING = "str"
|
||||||
DATETIME = "datetime"
|
DATETIME = "datetime"
|
||||||
FLOAT = "float"
|
FLOAT = "float"
|
||||||
|
|||||||
@@ -30,25 +30,39 @@ def get_backup_name(tabs: list[str], backup_prefix: str):
|
|||||||
return table_back_name
|
return table_back_name
|
||||||
|
|
||||||
|
|
||||||
def get_last_sequence_ids(engine, trade_back_name: str, order_back_name: str):
|
def get_last_sequence_ids(engine, sequence_name: str, table_back_name: str) -> int | None:
|
||||||
order_id: int | None = None
|
last_id: int | None = None
|
||||||
trade_id: int | None = None
|
|
||||||
|
|
||||||
if engine.name == "postgresql":
|
if engine.name == "postgresql":
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
trade_id = connection.execute(text("select nextval('trades_id_seq')")).fetchone()[0]
|
last_id = connection.execute(text(f"select nextval('{sequence_name}')")).fetchone()[0]
|
||||||
order_id = connection.execute(text("select nextval('orders_id_seq')")).fetchone()[0]
|
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
connection.execute(
|
connection.execute(
|
||||||
text(f"ALTER SEQUENCE orders_id_seq rename to {order_back_name}_id_seq_bak")
|
text(f"ALTER SEQUENCE {sequence_name} rename to {table_back_name}_id_seq_bak")
|
||||||
)
|
)
|
||||||
connection.execute(
|
|
||||||
text(f"ALTER SEQUENCE trades_id_seq rename to {trade_back_name}_id_seq_bak")
|
return last_id
|
||||||
)
|
|
||||||
return order_id, trade_id
|
|
||||||
|
|
||||||
|
|
||||||
def set_sequence_ids(engine, order_id, trade_id, pairlock_id=None):
|
def set_sequence_ids(
|
||||||
|
engine,
|
||||||
|
order_id: int | None = None,
|
||||||
|
trade_id: int | None = None,
|
||||||
|
pairlock_id: int | None = None,
|
||||||
|
kv_id: int | None = None,
|
||||||
|
custom_data_id: int | None = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Set sequence ids to the given values.
|
||||||
|
The id's given should be the next id to use, so the current max id + 1 - or current id
|
||||||
|
if using nextval before migration.
|
||||||
|
:param engine: SQLAlchemy engine
|
||||||
|
:param order_id: value to set for orders_id_seq (optional)
|
||||||
|
:param trade_id: value to set for trades_id_seq (optional)
|
||||||
|
:param pairlock_id: value to set for pairlocks_id_seq (optional)
|
||||||
|
:param kv_id: value to set for KeyValueStore_id_seq (optional)
|
||||||
|
:param custom_data_id: value to set for trade_custom_data_id_seq (optional)
|
||||||
|
"""
|
||||||
if engine.name == "postgresql":
|
if engine.name == "postgresql":
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
if order_id:
|
if order_id:
|
||||||
@@ -59,6 +73,14 @@ def set_sequence_ids(engine, order_id, trade_id, pairlock_id=None):
|
|||||||
connection.execute(
|
connection.execute(
|
||||||
text(f"ALTER SEQUENCE pairlocks_id_seq RESTART WITH {pairlock_id}")
|
text(f"ALTER SEQUENCE pairlocks_id_seq RESTART WITH {pairlock_id}")
|
||||||
)
|
)
|
||||||
|
if kv_id:
|
||||||
|
connection.execute(
|
||||||
|
text(f'ALTER SEQUENCE "KeyValueStore_id_seq" RESTART WITH {kv_id}')
|
||||||
|
)
|
||||||
|
if custom_data_id:
|
||||||
|
connection.execute(
|
||||||
|
text(f"ALTER SEQUENCE trade_custom_data_id_seq RESTART WITH {custom_data_id}")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def drop_index_on_table(engine, inspector, table_bak_name):
|
def drop_index_on_table(engine, inspector, table_bak_name):
|
||||||
@@ -157,7 +179,8 @@ def migrate_trades_and_orders_table(
|
|||||||
|
|
||||||
drop_index_on_table(engine, inspector, trade_back_name)
|
drop_index_on_table(engine, inspector, trade_back_name)
|
||||||
|
|
||||||
order_id, trade_id = get_last_sequence_ids(engine, trade_back_name, order_back_name)
|
order_id = get_last_sequence_ids(engine, "order_id_seq", order_back_name)
|
||||||
|
trade_id = get_last_sequence_ids(engine, "trades_id_seq", trade_back_name)
|
||||||
|
|
||||||
drop_orders_table(engine, order_back_name)
|
drop_orders_table(engine, order_back_name)
|
||||||
|
|
||||||
@@ -269,6 +292,7 @@ def migrate_pairlocks_table(decl_base, inspector, engine, pairlock_back_name: st
|
|||||||
connection.execute(text(f"alter table pairlocks rename to {pairlock_back_name}"))
|
connection.execute(text(f"alter table pairlocks rename to {pairlock_back_name}"))
|
||||||
|
|
||||||
drop_index_on_table(engine, inspector, pairlock_back_name)
|
drop_index_on_table(engine, inspector, pairlock_back_name)
|
||||||
|
pairlock_id = get_last_sequence_ids(engine, "pairlocks_id_seq", pairlock_back_name)
|
||||||
|
|
||||||
side = get_column_def(cols, "side", "'*'")
|
side = get_column_def(cols, "side", "'*'")
|
||||||
|
|
||||||
@@ -288,6 +312,8 @@ def migrate_pairlocks_table(decl_base, inspector, engine, pairlock_back_name: st
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set_sequence_ids(engine, pairlock_id=pairlock_id)
|
||||||
|
|
||||||
|
|
||||||
def set_sqlite_to_wal(engine):
|
def set_sqlite_to_wal(engine):
|
||||||
if engine.name == "sqlite" and str(engine.url) != "sqlite://":
|
if engine.name == "sqlite" and str(engine.url) != "sqlite://":
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class PairLocks:
|
|||||||
lock
|
lock
|
||||||
for lock in PairLocks.locks
|
for lock in PairLocks.locks
|
||||||
if (
|
if (
|
||||||
lock.lock_end_time >= now
|
lock.lock_end_time > now
|
||||||
and lock.active is True
|
and lock.active is True
|
||||||
and (pair is None or lock.pair == pair)
|
and (pair is None or lock.pair == pair)
|
||||||
and (side is None or lock.side == "*" or lock.side == side)
|
and (side is None or lock.side == "*" or lock.side == side)
|
||||||
|
|||||||
@@ -261,10 +261,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
if trades is not None and len(trades) > 0:
|
if trades is not None and len(trades) > 0:
|
||||||
# Create description for exit summarizing the trade
|
# Create description for exit summarizing the trade
|
||||||
trades["desc"] = trades.apply(
|
trades["desc"] = trades.apply(
|
||||||
lambda row: f"{row['profit_ratio']:.2%}, "
|
lambda row: (
|
||||||
+ (f"{row['enter_tag']}, " if row["enter_tag"] is not None else "")
|
f"{row['profit_ratio']:.2%}, "
|
||||||
+ f"{row['exit_reason']}, "
|
+ (f"{row['enter_tag']}, " if row["enter_tag"] is not None else "")
|
||||||
+ f"{row['trade_duration']} min",
|
+ f"{row['exit_reason']}, "
|
||||||
|
+ f"{row['trade_duration']} min"
|
||||||
|
),
|
||||||
axis=1,
|
axis=1,
|
||||||
)
|
)
|
||||||
trade_entries = go.Scatter(
|
trade_entries = go.Scatter(
|
||||||
|
|||||||
@@ -51,15 +51,6 @@ class AgeFilter(IPairList):
|
|||||||
f"({candle_limit})"
|
f"({candle_limit})"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def needstickers(self) -> bool:
|
|
||||||
"""
|
|
||||||
Boolean property defining if tickers are necessary.
|
|
||||||
If no Pairlist requires tickers, an empty Dict is passed
|
|
||||||
as tickers argument to filter_pairlist
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""Cross Market pair list filter"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from freqtrade.constants import PairPrefixes
|
||||||
|
from freqtrade.exchange.exchange_types import Tickers
|
||||||
|
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||||
|
from freqtrade.util import FtTTLCache
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CrossMarketPairList(IPairList):
|
||||||
|
is_pairlist_generator = True
|
||||||
|
supports_backtesting = SupportsBacktesting.BIASED
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self._pairs_exist_on: str = self._pairlistconfig.get("pairs_exist_on", "both_markets")
|
||||||
|
self._stake_currency: str = self._config["stake_currency"]
|
||||||
|
self._target_mode = "spot" if self._config["trading_mode"] == "futures" else "futures"
|
||||||
|
self._refresh_period = self._pairlistconfig.get("refresh_period", 1800)
|
||||||
|
self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1, ttl=self._refresh_period)
|
||||||
|
|
||||||
|
def short_desc(self) -> str:
|
||||||
|
"""
|
||||||
|
Short whitelist method description - used for startup-messages
|
||||||
|
"""
|
||||||
|
pairs_exist_on = self._pairs_exist_on
|
||||||
|
msg = f"{self.name} - Pairs that exists on {pairs_exist_on.capitalize()}."
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def description() -> str:
|
||||||
|
return "Filter pairs if they exist or not on another market."
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def available_parameters() -> dict[str, PairlistParameter]:
|
||||||
|
return {
|
||||||
|
"pairs_exist_on": {
|
||||||
|
"type": "option",
|
||||||
|
"default": "both_markets",
|
||||||
|
"options": ["current_market_only", "both_markets"],
|
||||||
|
"description": "Mode of operation",
|
||||||
|
"help": "Mode of operation (current_market_only/both_markets)",
|
||||||
|
},
|
||||||
|
**IPairList.refresh_period_parameter(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_base_list(self) -> list[str]:
|
||||||
|
target_mode = self._target_mode
|
||||||
|
spot_only = True if target_mode == "spot" else False
|
||||||
|
futures_only = True if target_mode == "futures" else False
|
||||||
|
bases = [
|
||||||
|
v.get("base", "")
|
||||||
|
for _, v in self._exchange.get_markets(
|
||||||
|
quote_currencies=[self._stake_currency],
|
||||||
|
tradable_only=False,
|
||||||
|
active_only=True,
|
||||||
|
spot_only=spot_only,
|
||||||
|
futures_only=futures_only,
|
||||||
|
).items()
|
||||||
|
]
|
||||||
|
return bases
|
||||||
|
|
||||||
|
def gen_pairlist(self, tickers: Tickers) -> list[str]:
|
||||||
|
"""
|
||||||
|
Generate the pairlist
|
||||||
|
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
||||||
|
:return: List of pairs
|
||||||
|
"""
|
||||||
|
# Generate dynamic whitelist
|
||||||
|
# Must always run if this pairlist is the first in the list.
|
||||||
|
pairlist = self._pair_cache.get("pairlist")
|
||||||
|
if pairlist:
|
||||||
|
# Item found - no refresh necessary
|
||||||
|
return pairlist.copy()
|
||||||
|
else:
|
||||||
|
# Use fresh pairlist
|
||||||
|
# Check if pair quote currency equals to the stake currency.
|
||||||
|
_pairlist = [
|
||||||
|
k
|
||||||
|
for k in self._exchange.get_markets(
|
||||||
|
quote_currencies=[self._stake_currency], tradable_only=True, active_only=True
|
||||||
|
).keys()
|
||||||
|
]
|
||||||
|
|
||||||
|
_pairlist = self.verify_blacklist(_pairlist, logger.info)
|
||||||
|
|
||||||
|
pairlist = self.filter_pairlist(_pairlist, tickers)
|
||||||
|
self._pair_cache["pairlist"] = pairlist.copy()
|
||||||
|
|
||||||
|
return pairlist
|
||||||
|
|
||||||
|
def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]:
|
||||||
|
bases = self.get_base_list()
|
||||||
|
pairs_exist_on = self._pairs_exist_on
|
||||||
|
is_whitelist_mode = pairs_exist_on == "both_markets"
|
||||||
|
whitelisted_pairlist: list[str] = []
|
||||||
|
filtered_pairlist = pairlist.copy()
|
||||||
|
|
||||||
|
for pair in pairlist:
|
||||||
|
base = self._exchange.get_pair_base_currency(pair)
|
||||||
|
if not base:
|
||||||
|
self.log_once(
|
||||||
|
f"Unable to get base currency for pair {pair}, skipping it.", logger.warning
|
||||||
|
)
|
||||||
|
filtered_pairlist.remove(pair)
|
||||||
|
continue
|
||||||
|
found_in_bases = base in bases
|
||||||
|
if not found_in_bases:
|
||||||
|
for prefix in PairPrefixes:
|
||||||
|
# Check in case of PEPE needs to be changed into 1000PEPE for example
|
||||||
|
test_prefix = f"{prefix}{base}"
|
||||||
|
found_in_bases = test_prefix in bases
|
||||||
|
if found_in_bases:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Avoid false positive since there are KAVA and AVA pairs, which aren't related
|
||||||
|
if prefix != "K":
|
||||||
|
# Check in case of 1000PEPE needs to be changed into PEPE for example
|
||||||
|
if base.startswith(prefix):
|
||||||
|
temp_base = base.removeprefix(prefix)
|
||||||
|
found_in_bases = temp_base in bases
|
||||||
|
if found_in_bases:
|
||||||
|
break
|
||||||
|
if found_in_bases:
|
||||||
|
whitelisted_pairlist.append(pair)
|
||||||
|
filtered_pairlist.remove(pair)
|
||||||
|
|
||||||
|
return whitelisted_pairlist if is_whitelist_mode else filtered_pairlist
|
||||||
@@ -28,15 +28,6 @@ class DelistFilter(IPairList):
|
|||||||
"DelistFilter doesn't support this exchange and trading mode combination.",
|
"DelistFilter doesn't support this exchange and trading mode combination.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def needstickers(self) -> bool:
|
|
||||||
"""
|
|
||||||
Boolean property defining if tickers are necessary.
|
|
||||||
If no Pairlist requires tickers, an empty Dict is passed
|
|
||||||
as tickers argument to filter_pairlist
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
|
|||||||
@@ -15,15 +15,6 @@ logger = logging.getLogger(__name__)
|
|||||||
class FullTradesFilter(IPairList):
|
class FullTradesFilter(IPairList):
|
||||||
supports_backtesting = SupportsBacktesting.NO_ACTION
|
supports_backtesting = SupportsBacktesting.NO_ACTION
|
||||||
|
|
||||||
@property
|
|
||||||
def needstickers(self) -> bool:
|
|
||||||
"""
|
|
||||||
Boolean property defining if tickers are necessary.
|
|
||||||
If no Pairlist requires tickers, an empty List is passed
|
|
||||||
as tickers argument to filter_pairlist
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short allowlist method description - used for startup-messages
|
Short allowlist method description - used for startup-messages
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ PairList Handler base class
|
|||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
from typing import Any, Literal, TypedDict
|
from typing import Any, Literal, TypedDict
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
@@ -58,7 +58,7 @@ PairlistParameter = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SupportsBacktesting(str, Enum):
|
class SupportsBacktesting(StrEnum):
|
||||||
"""
|
"""
|
||||||
Enum to indicate if a Pairlist Handler supports backtesting.
|
Enum to indicate if a Pairlist Handler supports backtesting.
|
||||||
"""
|
"""
|
||||||
@@ -107,7 +107,6 @@ class IPairList(LoggingMixin, ABC):
|
|||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Boolean property defining if tickers are necessary.
|
Boolean property defining if tickers are necessary.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Provides dynamic pair list based on Market Cap
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
from freqtrade.constants import PairPrefixes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.exchange_types import Tickers
|
from freqtrade.exchange.exchange_types import Tickers
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||||
@@ -64,15 +65,6 @@ class MarketCapPairList(IPairList):
|
|||||||
"Please ensure this value is necessary for your use case.",
|
"Please ensure this value is necessary for your use case.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def needstickers(self) -> bool:
|
|
||||||
"""
|
|
||||||
Boolean property defining if tickers are necessary.
|
|
||||||
If no Pairlist requires tickers, an empty Dict is passed
|
|
||||||
as tickers argument to filter_pairlist
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
@@ -162,9 +154,6 @@ class MarketCapPairList(IPairList):
|
|||||||
|
|
||||||
return pairlist
|
return pairlist
|
||||||
|
|
||||||
# Prefixes to test to discover coins like 1000PEPE/USDDT:USDT or KPEPE/USDC (hyperliquid)
|
|
||||||
prefixes = ("1000", "K")
|
|
||||||
|
|
||||||
def resolve_marketcap_pair(
|
def resolve_marketcap_pair(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
@@ -179,7 +168,7 @@ class MarketCapPairList(IPairList):
|
|||||||
return pair
|
return pair
|
||||||
|
|
||||||
if pair not in markets:
|
if pair not in markets:
|
||||||
for prefix in self.prefixes:
|
for prefix in PairPrefixes:
|
||||||
test_prefix = f"{prefix}{pair}"
|
test_prefix = f"{prefix}{pair}"
|
||||||
|
|
||||||
if test_prefix in pairlist:
|
if test_prefix in pairlist:
|
||||||
|
|||||||
@@ -24,15 +24,6 @@ class OffsetFilter(IPairList):
|
|||||||
if self._offset < 0:
|
if self._offset < 0:
|
||||||
raise OperationalException("OffsetFilter requires offset to be >= 0")
|
raise OperationalException("OffsetFilter requires offset to be >= 0")
|
||||||
|
|
||||||
@property
|
|
||||||
def needstickers(self) -> bool:
|
|
||||||
"""
|
|
||||||
Boolean property defining if tickers are necessary.
|
|
||||||
If no Pairlist requires tickers, an empty Dict is passed
|
|
||||||
as tickers argument to filter_pairlist
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
|
|||||||
@@ -25,15 +25,6 @@ class PerformanceFilter(IPairList):
|
|||||||
self._minutes = self._pairlistconfig.get("minutes", 0)
|
self._minutes = self._pairlistconfig.get("minutes", 0)
|
||||||
self._min_profit = self._pairlistconfig.get("min_profit")
|
self._min_profit = self._pairlistconfig.get("min_profit")
|
||||||
|
|
||||||
@property
|
|
||||||
def needstickers(self) -> bool:
|
|
||||||
"""
|
|
||||||
Boolean property defining if tickers are necessary.
|
|
||||||
If no Pairlist requires tickers, an empty List is passed
|
|
||||||
as tickers argument to filter_pairlist
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short allowlist method description - used for startup-messages
|
Short allowlist method description - used for startup-messages
|
||||||
|
|||||||
@@ -42,15 +42,6 @@ class ProducerPairList(IPairList):
|
|||||||
"ProducerPairList requires external_message_consumer to be enabled."
|
"ProducerPairList requires external_message_consumer to be enabled."
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def needstickers(self) -> bool:
|
|
||||||
"""
|
|
||||||
Boolean property defining if tickers are necessary.
|
|
||||||
If no Pairlist requires tickers, an empty Dict is passed
|
|
||||||
as tickers argument to filter_pairlist
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
|
|||||||
@@ -31,12 +31,6 @@ class RemotePairList(IPairList):
|
|||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if "number_assets" not in self._pairlistconfig:
|
|
||||||
raise OperationalException(
|
|
||||||
"`number_assets` not specified. Please check your configuration "
|
|
||||||
'for "pairlist.config.number_assets"'
|
|
||||||
)
|
|
||||||
|
|
||||||
if "pairlist_url" not in self._pairlistconfig:
|
if "pairlist_url" not in self._pairlistconfig:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"`pairlist_url` not specified. Please check your configuration "
|
"`pairlist_url` not specified. Please check your configuration "
|
||||||
@@ -45,7 +39,7 @@ class RemotePairList(IPairList):
|
|||||||
|
|
||||||
self._mode = self._pairlistconfig.get("mode", "whitelist")
|
self._mode = self._pairlistconfig.get("mode", "whitelist")
|
||||||
self._processing_mode = self._pairlistconfig.get("processing_mode", "filter")
|
self._processing_mode = self._pairlistconfig.get("processing_mode", "filter")
|
||||||
self._number_pairs = self._pairlistconfig["number_assets"]
|
self._number_pairs: int | None = self._pairlistconfig.get("number_assets", None)
|
||||||
self._refresh_period: int = self._pairlistconfig.get("refresh_period", 1800)
|
self._refresh_period: int = self._pairlistconfig.get("refresh_period", 1800)
|
||||||
self._keep_pairlist_on_failure = self._pairlistconfig.get("keep_pairlist_on_failure", True)
|
self._keep_pairlist_on_failure = self._pairlistconfig.get("keep_pairlist_on_failure", True)
|
||||||
self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1, ttl=self._refresh_period)
|
self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1, ttl=self._refresh_period)
|
||||||
@@ -72,20 +66,11 @@ class RemotePairList(IPairList):
|
|||||||
"position of your pairlist."
|
"position of your pairlist."
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def needstickers(self) -> bool:
|
|
||||||
"""
|
|
||||||
Boolean property defining if tickers are necessary.
|
|
||||||
If no Pairlist requires tickers, an empty Dict is passed
|
|
||||||
as tickers argument to filter_pairlist
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
"""
|
"""
|
||||||
return f"{self.name} - {self._pairlistconfig['number_assets']} pairs from RemotePairlist."
|
return f"{self.name} - {self._number_pairs or 'all'} pairs from RemotePairlist."
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def description() -> str:
|
def description() -> str:
|
||||||
@@ -102,7 +87,7 @@ class RemotePairList(IPairList):
|
|||||||
},
|
},
|
||||||
"number_assets": {
|
"number_assets": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"default": 30,
|
"default": None,
|
||||||
"description": "Number of assets",
|
"description": "Number of assets",
|
||||||
"help": "Number of assets to use from the pairlist.",
|
"help": "Number of assets to use from the pairlist.",
|
||||||
},
|
},
|
||||||
@@ -257,7 +242,8 @@ class RemotePairList(IPairList):
|
|||||||
|
|
||||||
pairlist = expand_pairlist(pairlist, list(self._exchange.get_markets().keys()))
|
pairlist = expand_pairlist(pairlist, list(self._exchange.get_markets().keys()))
|
||||||
pairlist = self._whitelist_for_active_markets(pairlist)
|
pairlist = self._whitelist_for_active_markets(pairlist)
|
||||||
pairlist = pairlist[: self._number_pairs]
|
if self._number_pairs and (self._mode == "whitelist"):
|
||||||
|
pairlist = pairlist[: self._number_pairs]
|
||||||
|
|
||||||
if pairlist:
|
if pairlist:
|
||||||
self._pair_cache["pairlist"] = pairlist.copy()
|
self._pair_cache["pairlist"] = pairlist.copy()
|
||||||
@@ -314,5 +300,6 @@ class RemotePairList(IPairList):
|
|||||||
if filtered:
|
if filtered:
|
||||||
self.log_once(f"Blacklist - Filtered out pairs: {filtered}", logger.info)
|
self.log_once(f"Blacklist - Filtered out pairs: {filtered}", logger.info)
|
||||||
|
|
||||||
merged_list = merged_list[: self._number_pairs]
|
if self._number_pairs and (self._mode == "whitelist"):
|
||||||
|
merged_list = merged_list[: self._number_pairs]
|
||||||
return merged_list
|
return merged_list
|
||||||
|
|||||||
@@ -39,15 +39,6 @@ class ShuffleFilter(IPairList):
|
|||||||
maxsize=1000, ttl=timeframe_to_seconds(self._config["timeframe"])
|
maxsize=1000, ttl=timeframe_to_seconds(self._config["timeframe"])
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def needstickers(self) -> bool:
|
|
||||||
"""
|
|
||||||
Boolean property defining if tickers are necessary.
|
|
||||||
If no Pairlist requires tickers, an empty Dict is passed
|
|
||||||
as tickers argument to filter_pairlist
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
|
|||||||
@@ -28,15 +28,6 @@ class StaticPairList(IPairList):
|
|||||||
# Pair cache - only used for optimize modes
|
# Pair cache - only used for optimize modes
|
||||||
self._bt_pair_cache: LRUCache = LRUCache(maxsize=1)
|
self._bt_pair_cache: LRUCache = LRUCache(maxsize=1)
|
||||||
|
|
||||||
@property
|
|
||||||
def needstickers(self) -> bool:
|
|
||||||
"""
|
|
||||||
Boolean property defining if tickers are necessary.
|
|
||||||
If no Pairlist requires tickers, an empty Dict is passed
|
|
||||||
as tickers argument to filter_pairlist
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
|
|||||||
@@ -53,15 +53,6 @@ class VolatilityFilter(IPairList):
|
|||||||
"either None (undefined), 'asc' or 'desc'"
|
"either None (undefined), 'asc' or 'desc'"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def needstickers(self) -> bool:
|
|
||||||
"""
|
|
||||||
Boolean property defining if tickers are necessary.
|
|
||||||
If no Pairlist requires tickers, an empty List is passed
|
|
||||||
as tickers argument to filter_pairlist
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ def expand_pairlist(
|
|||||||
raise ValueError(f"Wildcard error in {pair_wc}, {err}")
|
raise ValueError(f"Wildcard error in {pair_wc}, {err}")
|
||||||
|
|
||||||
# Remove wildcard pairs that didn't have a match.
|
# Remove wildcard pairs that didn't have a match.
|
||||||
result = [element for element in result if re.fullmatch(r"^[A-Za-z0-9:/-]+$", element)]
|
result = [
|
||||||
|
element
|
||||||
|
for element in result
|
||||||
|
if re.fullmatch(r"^[\w:/-]+$", element) and "_" not in element
|
||||||
|
]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for pair_wc in wildcardpl:
|
for pair_wc in wildcardpl:
|
||||||
|
|||||||
@@ -47,15 +47,6 @@ class RangeStabilityFilter(IPairList):
|
|||||||
"either None (undefined), 'asc' or 'desc'"
|
"either None (undefined), 'asc' or 'desc'"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def needstickers(self) -> bool:
|
|
||||||
"""
|
|
||||||
Boolean property defining if tickers are necessary.
|
|
||||||
If no Pairlist requires tickers, an empty List is passed
|
|
||||||
as tickers argument to filter_pairlist
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user