Compare commits
579 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bae2f98e5f | |||
| e88e3617c7 | |||
| 7cd97d9cfd | |||
| 12a9f0e1b1 | |||
| 6bb78edd96 | |||
| 98b56a49c8 | |||
| 791fbe8054 | |||
| cedfb2a51f | |||
| 93b6c52aad | |||
| 0ef85e161e | |||
| 26aed91bbf | |||
| 3c2ddda203 | |||
| beec9123d4 | |||
| 360f294f45 | |||
| a64ab2629b | |||
| 5d88923e03 | |||
| 84cbb1eb01 | |||
| 1c764d622d | |||
| fb6627335b | |||
| 436d3b1bbc | |||
| 308c80c7b2 | |||
| a4a69bd16d | |||
| 9bd68dd63a | |||
| 670cfa2087 | |||
| 7a69cfa62b | |||
| 142dd3fd25 | |||
| d102d3cac8 | |||
| 2b7bcdb594 | |||
| 9ccbf15080 | |||
| 16717f06e9 | |||
| 7d77e16271 | |||
| f501619434 | |||
| 7c9a618c72 | |||
| 16374ef7f1 | |||
| bb4d2f8f1a | |||
| 52fc180c75 | |||
| 87b313cd53 | |||
| 74c88f77c0 | |||
| 6cb459f37b | |||
| 00848c9a96 | |||
| e8adf01b18 | |||
| d28fce7ed8 | |||
| 390d504cf2 | |||
| 1799aa33f9 | |||
| dcff1c4317 | |||
| 121bdab8fe | |||
| 9a48feea35 | |||
| 0955ccadfe | |||
| c778e44e80 | |||
| 18933a6e20 | |||
| 182bdaceb5 | |||
| 6ae4394628 | |||
| 1bdb1539b1 | |||
| 2765277cd0 | |||
| 085a9be186 | |||
| e279c0333f | |||
| 5e7246d23b | |||
| fe86163730 | |||
| b5d467c943 | |||
| c9fd8a619a | |||
| 690080e180 | |||
| 583431e9d5 | |||
| 4b82f82d9c | |||
| 63d6001415 | |||
| ad6cf90037 | |||
| 558c65a85c | |||
| 8cb72e1fa3 | |||
| 4870d45b04 | |||
| 4436acea68 | |||
| e8eb6d7018 | |||
| f1a6e61dbd | |||
| 4abb871c16 | |||
| a6eccaec69 | |||
| 1747724206 | |||
| 616900330b | |||
| bbe5e22730 | |||
| 8b396ae3a2 | |||
| 7f511cbe82 | |||
| 12f5b9bc5b | |||
| d152e3e47d | |||
| 39908c60e2 | |||
| 74a1e48a1c | |||
| b507aebf1c | |||
| ad293c28d0 | |||
| e4c7b5511a | |||
| 85fd070899 | |||
| e658490593 | |||
| 621e78e7ce | |||
| fc5801a0b7 | |||
| 22cd1357f8 | |||
| ead68e0a51 | |||
| 80b87d2592 | |||
| 0ac43042cc | |||
| 590a69ca42 | |||
| e44a36cedd | |||
| 131ab77085 | |||
| ba07de8d00 | |||
| 6ee34aa482 | |||
| d5686d548c | |||
| 4917a0abe5 | |||
| 93f2cf5db7 | |||
| 38a470098f | |||
| b0eb5c77ec | |||
| 0d5bd94f43 | |||
| 735284dccd | |||
| deb0dd80c8 | |||
| 12f815e157 | |||
| 059636f1ea | |||
| 793ca9c3da | |||
| 323a28c0bc | |||
| 77567f6315 | |||
| dda448c10d | |||
| b7df0de33c | |||
| a89ef23407 | |||
| b90538f47d | |||
| ee6b7f2f95 | |||
| b4aa6231e0 | |||
| 6bd57cc3b5 | |||
| 26db014782 | |||
| 8e3ca302b0 | |||
| 14aeb56ff9 | |||
| 9cbb8ec1c2 | |||
| faa5c4be39 | |||
| 45b6ec95a4 | |||
| 91b921cc88 | |||
| 123897ea89 | |||
| 31cf2dd46d | |||
| 06a53924d7 | |||
| 993771c5d3 | |||
| f08f19d1cb | |||
| 515912f14d | |||
| df8465a9f3 | |||
| 1333cb3181 | |||
| 18fa981690 | |||
| 60425e6237 | |||
| 3b5f1d52cd | |||
| 519721b58b | |||
| 3ae7823983 | |||
| 7182e6960e | |||
| 8596042065 | |||
| c2e92250b5 | |||
| d2ed5c6736 | |||
| 8e023a7bf3 | |||
| b3711fdf6c | |||
| 681876e0e5 | |||
| c3502991d3 | |||
| 6cc22fcf74 | |||
| 2f011e5421 | |||
| 4322acbbb8 | |||
| 50cba51008 | |||
| 7498f20baa | |||
| a1b3e59491 | |||
| 4fcbce2ce5 | |||
| 22699ee6ed | |||
| aec5450036 | |||
| 2c5a150a7b | |||
| f845dc71b1 | |||
| 61274252af | |||
| 00d903cb3f | |||
| 29f235441b | |||
| 872b59c9cd | |||
| be7f5f4e26 | |||
| 82c1f096de | |||
| ed7887c426 | |||
| e58f7b0344 | |||
| 32eaae5dcf | |||
| 38c74e5d1a | |||
| e89a31c6d1 | |||
| 3eb8de633c | |||
| 0ee3e4304f | |||
| 228dfe8ab2 | |||
| bfcc2aa153 | |||
| 39e2ecb6a2 | |||
| 0fcfd933ea | |||
| b9a4918d61 | |||
| 0d7127bb6a | |||
| cfd048e74e | |||
| c911332da3 | |||
| 43a849d45d | |||
| 3ad502b073 | |||
| 6d4ad393d5 | |||
| 69cc238700 | |||
| 9a9b4e15f3 | |||
| fcab9460fb | |||
| 58130572a9 | |||
| f82d2fe796 | |||
| 577464c12d | |||
| 00bd3e5a5c | |||
| bc2be74b36 | |||
| 3543437fc5 | |||
| 4b278941b8 | |||
| 8a33b71351 | |||
| a09ca38de2 | |||
| 4d4fee9874 | |||
| f2dfe1788a | |||
| 0e82c0458a | |||
| 2ec2626277 | |||
| 04f4f327df | |||
| 34ad58e2da | |||
| f78aa7b39a | |||
| a174461952 | |||
| 6c164683b7 | |||
| 9cdd8659ec | |||
| 795df94817 | |||
| 40f87c0d30 | |||
| 1380940565 | |||
| 595309c9ed | |||
| 78dde7d1bc | |||
| 2a0da08413 | |||
| fd7aff6063 | |||
| c41584a548 | |||
| 1e6d832f71 | |||
| 6f2ba13b0c | |||
| 78141c7216 | |||
| 8e91fea11f | |||
| 0f8c6d456e | |||
| 2a1b3ae7ba | |||
| 0a94a6d332 | |||
| 75a3651493 | |||
| 0dad2b36ab | |||
| 70834b6491 | |||
| 31269c7ce0 | |||
| f0a5bbe511 | |||
| 863124d67e | |||
| 6d1e4d8f45 | |||
| 5f12c81768 | |||
| 31341afcef | |||
| 383b305b74 | |||
| 84244e7ca7 | |||
| cc06e9baec | |||
| d5ac8a5b0b | |||
| 00a4a743d2 | |||
| 650cb1ef8c | |||
| 76ea4ea14f | |||
| 90502a6ac0 | |||
| f9fe909847 | |||
| 7085103e43 | |||
| c5ee96b66a | |||
| d454a10dea | |||
| e3d03f586d | |||
| e8d748535f | |||
| 1cdb386374 | |||
| 8205cb25b8 | |||
| 511b817e84 | |||
| de94170424 | |||
| 6e9b60c0b0 | |||
| b8cf38cb6f | |||
| b394705545 | |||
| 9b99c5b3f3 | |||
| e77e901df1 | |||
| b26c37af62 | |||
| 414fe81b27 | |||
| 00de8077f7 | |||
| 1dde1ad0ed | |||
| 2708343a2d | |||
| 61eb3306dd | |||
| e71329c82b | |||
| dc4adccaaf | |||
| 45b9c8c53f | |||
| 5c02ce934d | |||
| 260512d678 | |||
| 8b1b16cb08 | |||
| cacd52db1c | |||
| 4e809947c7 | |||
| 89c4aa23f3 | |||
| 1bd043a912 | |||
| 3ce23f99de | |||
| c5fa5c5c69 | |||
| 21d0b60168 | |||
| ba3925cdcd | |||
| 9cf6d47a43 | |||
| e01993b9c1 | |||
| c354a76ae2 | |||
| f296213088 | |||
| d80dbc1644 | |||
| 91ee8652ef | |||
| 75dfc95221 | |||
| 05fe35ce57 | |||
| 95e36013db | |||
| c5c96d9610 | |||
| ad9a7279be | |||
| dafb2fda7b | |||
| 04fea4ac29 | |||
| 896250fe88 | |||
| 8485abc9b8 | |||
| 2013a2edda | |||
| 11eb0c0802 | |||
| 78709a9d76 | |||
| f55a90d98c | |||
| 4b3ff02d49 | |||
| ea52deb98e | |||
| 7b33242aa0 | |||
| 46ee092119 | |||
| 931cd2e7ba | |||
| b6899264a8 | |||
| ca47882fa9 | |||
| c21ce5ef94 | |||
| 9baa6eebbe | |||
| 0feec257e3 | |||
| 802d42cc00 | |||
| f546146d40 | |||
| d2c4bd1b50 | |||
| 5e5495e050 | |||
| 51f3f0d65a | |||
| 0019867da8 | |||
| 335e632bbc | |||
| d0ac462dd3 | |||
| fc113f7bbf | |||
| 4f6b1b6fd0 | |||
| 006d2305af | |||
| 5c9f140c5d | |||
| c6f3b00819 | |||
| 1780d2db23 | |||
| 78d602f7b4 | |||
| 4823f66c4e | |||
| a3c19d10f7 | |||
| ebb318a0f0 | |||
| 35d6e6e832 | |||
| 37bc93e0d3 | |||
| 2cd30b0604 | |||
| 2f30cda9d7 | |||
| 9e1c760222 | |||
| 53884cae2f | |||
| 1b16b450b7 | |||
| 18ba3241ce | |||
| 6f8ad595b7 | |||
| 5479f204b5 | |||
| c54b94b5a4 | |||
| 5ed590d862 | |||
| 3f5ed399b5 | |||
| a9a686c64e | |||
| 978ca8f852 | |||
| ea30f906f9 | |||
| 811112f0ce | |||
| 5ebffb27aa | |||
| 0cb676a677 | |||
| 6a6dce323d | |||
| 0d62fab367 | |||
| a1b4a4dbfb | |||
| bcd033bd0d | |||
| 092da6aee5 | |||
| 13c63c0bf5 | |||
| aa38a36b56 | |||
| ce8732b0cd | |||
| bcf6c244b0 | |||
| 9023ca4f56 | |||
| e9847d9549 | |||
| 0beb76ce48 | |||
| 21d723b35d | |||
| f002ce67b1 | |||
| 5fdc8acbe9 | |||
| 1811f9581f | |||
| 66235f3198 | |||
| 6c8b8f642a | |||
| 29b7cbbb12 | |||
| b2d5343a4e | |||
| 6905b8059d | |||
| 301aff141a | |||
| 245ca4bc2a | |||
| f32d25daac | |||
| 1143aba671 | |||
| 278bd0e97d | |||
| d5b296c75c | |||
| c63fba2e57 | |||
| c8d74baaa1 | |||
| a9ebff3965 | |||
| 8859b9009a | |||
| d4aee7e433 | |||
| f0908a1043 | |||
| cf6bf1b7b5 | |||
| 84e9251fcd | |||
| 5800002d42 | |||
| 6a98c19dab | |||
| 025707d9ad | |||
| 4540e2ceea | |||
| eb3e8b82f9 | |||
| 98da54ae82 | |||
| fa5210aa4b | |||
| 289e53ed5a | |||
| 789f4aa4f8 | |||
| 2045fa6427 | |||
| 8c62816972 | |||
| c0d40a03aa | |||
| 551831c901 | |||
| 67850fd2a2 | |||
| 715ef21da0 | |||
| b8defa5d45 | |||
| 6d3dac828a | |||
| 968a9a3750 | |||
| 03b5cdb240 | |||
| 7dc0d5d146 | |||
| c20f511418 | |||
| 484b3ff2f3 | |||
| 5d25adfc42 | |||
| 1099fd2525 | |||
| 293989f019 | |||
| 57d1e24275 | |||
| e52b5aef45 | |||
| a16d2a1ef9 | |||
| 13c86452e9 | |||
| fe18731057 | |||
| e26529b695 | |||
| bac6219cc1 | |||
| 0ed3bdc747 | |||
| 8af0631ff0 | |||
| 2e3d276304 | |||
| bd5630a104 | |||
| b406219515 | |||
| 31d3a19836 | |||
| 83b372a32d | |||
| 51e0b204b6 | |||
| 6848f9197e | |||
| 6d2c30abca | |||
| 93bde7dc46 | |||
| 97e2e0a405 | |||
| 12206f028b | |||
| 4c7944ac77 | |||
| d4ced7b416 | |||
| 451eef5c99 | |||
| ae8f059de0 | |||
| 878bd7cbc7 | |||
| 6d017c9a6c | |||
| 4c3d9b8c70 | |||
| 23a4260859 | |||
| d15d08a2d5 | |||
| 6aeab16ce4 | |||
| 46538d9a5b | |||
| f0f48395c5 | |||
| b3a1442a69 | |||
| bbafb1dabd | |||
| 645a9159e4 | |||
| 2d3ff2f8ca | |||
| 5919736904 | |||
| 732610e200 | |||
| 309985503d | |||
| 072ed705fc | |||
| 3689b52309 | |||
| 01fbf31405 | |||
| 00f687f3f5 | |||
| 96849fcafe | |||
| 80d5b6e24c | |||
| f33fd98c83 | |||
| c7636734de | |||
| 994e61fe42 | |||
| 359eba462b | |||
| 38e48c0c5e | |||
| 1e187e0945 | |||
| e4fc5df1cf | |||
| cde886b884 | |||
| 9f4e167455 | |||
| f5e6504e22 | |||
| 0ec1066b34 | |||
| 62d4da3b94 | |||
| 253950deb6 | |||
| ed92d6beb9 | |||
| ebb362d9fa | |||
| f23fad420e | |||
| aaf23221ff | |||
| 156c1a99a9 | |||
| cb55ef5c59 | |||
| 6540fbb8e7 | |||
| 412392aea9 | |||
| e6030b7f59 | |||
| c1c968666e | |||
| f8d6363d19 | |||
| acc69e0d2e | |||
| 5110d0bdde | |||
| 597cc0592b | |||
| 01b0a8fa42 | |||
| cf6b7a847b | |||
| 4897080827 | |||
| 3ca8e0fb5c | |||
| 40f4ff04c2 | |||
| 3bd911982f | |||
| 730383ab18 | |||
| b70f10dca6 | |||
| 17009ac59f | |||
| 07fbf2b467 | |||
| 3f0be5e41f | |||
| 2845568f61 | |||
| e3229935f6 | |||
| b1ee115b77 | |||
| d6060f04bc | |||
| 1ae5310d2f | |||
| 417a0817a7 | |||
| 41a82eff21 | |||
| d41acc77f7 | |||
| 271fc6b585 | |||
| 262f4ffa4c | |||
| 338c1c5424 | |||
| 8ca25b1757 | |||
| 68e54248fd | |||
| e792bafe21 | |||
| fe95581acd | |||
| 7f831f8a9c | |||
| 3fb7382cc4 | |||
| 83f70b3c27 | |||
| 922d5893f8 | |||
| e6e14ab67f | |||
| 37792e7d86 | |||
| a63ea6adb8 | |||
| 06a8f3d097 | |||
| e76b9bb64a | |||
| 606dbe5db1 | |||
| 81e4e0d301 | |||
| a3e97527d1 | |||
| 3ebc7bfcfc | |||
| c892b07eef | |||
| 7a23da4deb | |||
| 670999abda | |||
| e2b5e209f6 | |||
| 7137461891 | |||
| 10e7d279f0 | |||
| 971cb94ce4 | |||
| 19526f1df2 | |||
| 9c73c8671e | |||
| 0f5427f4a0 | |||
| a013793b2f | |||
| 1a77f1b203 | |||
| faf552837a | |||
| 4b2d099258 | |||
| 4e6ea1d2ba | |||
| 197623839d | |||
| 6d8fa76708 | |||
| d7eb49d5d0 | |||
| 659509e085 | |||
| 2b729c2527 | |||
| 38151423b8 | |||
| 2f990b773d | |||
| 20d253dd41 | |||
| 6e2165b5e5 | |||
| 6860904bb9 | |||
| 450dc23374 | |||
| d781f09b46 | |||
| dbde8332f7 | |||
| f5f1785b17 | |||
| 7d01928df0 | |||
| 31f712749b | |||
| bcdf83100f | |||
| dc88ea0ed9 | |||
| 543457a1ef | |||
| 5d781d0114 | |||
| 45b1dabe0a | |||
| ffea373f86 | |||
| ba650de562 | |||
| 39233ff240 | |||
| 8950423418 | |||
| 760a2b9ff7 | |||
| a13f488b84 | |||
| 09b1582704 | |||
| 61ab49a124 | |||
| 1a95b04d79 | |||
| 691a6b0a72 | |||
| 1d0a902251 | |||
| 1de9f5a2fb | |||
| 7a5342cc93 | |||
| 52c73eabeb | |||
| 02de1f0a1e | |||
| 3453bdf607 | |||
| ac2723c3a0 | |||
| 1536c09df3 | |||
| 645f70e216 | |||
| b59db3c690 | |||
| fade66afd9 | |||
| f63484d0b0 | |||
| 3543e96ec5 | |||
| c83ea0db4f | |||
| ad256367be | |||
| 73b427370b | |||
| ca7234e33f | |||
| cd7b267171 | |||
| 6cef73947c | |||
| 33dbe32e5c | |||
| 8caf7a888e | |||
| 033050e6cf | |||
| 0340d36af2 | |||
| ad7816d51e | |||
| 73127c8179 | |||
| ea6d23bd2d |
@@ -46,8 +46,9 @@ runs:
|
||||
id: tags
|
||||
env:
|
||||
BRANCH_NAME_INPUT: ${{ github.event.inputs.branch_name }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||
BRANCH_NAME="${BRANCH_NAME_INPUT}"
|
||||
else
|
||||
BRANCH_NAME="${GITHUB_REF##*/}"
|
||||
|
||||
+15
-8
@@ -2,7 +2,7 @@ version: 2
|
||||
updates:
|
||||
- package-ecosystem: docker
|
||||
cooldown:
|
||||
default-days: 4
|
||||
default-days: 7
|
||||
directories:
|
||||
- "/"
|
||||
- "/docker"
|
||||
@@ -16,7 +16,7 @@ updates:
|
||||
- package-ecosystem: devcontainers
|
||||
directory: "/"
|
||||
cooldown:
|
||||
default-days: 4
|
||||
default-days: 7
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
@@ -24,13 +24,13 @@ updates:
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
cooldown:
|
||||
default-days: 4
|
||||
default-days: 7
|
||||
exclude:
|
||||
- ccxt
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
interval: "cron"
|
||||
# Monday at 03:00
|
||||
cronjob: "0 3 * * 1"
|
||||
open-pull-requests-limit: 15
|
||||
target-branch: develop
|
||||
groups:
|
||||
@@ -51,8 +51,15 @@ updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
cooldown:
|
||||
default-days: 4
|
||||
default-days: 7
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "cron"
|
||||
# Monday at 03:00
|
||||
cronjob: "0 3 * * 1"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: develop
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
# Combine updates for github provided actions
|
||||
- "actions/*"
|
||||
|
||||
@@ -6,20 +6,25 @@ on:
|
||||
# on demand
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
auto-update:
|
||||
name: "Auto Update Binance Leverage Tiers"
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: develop
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -34,7 +39,7 @@ jobs:
|
||||
run: python build_helpers/binance_update_lev_tiers.py
|
||||
|
||||
|
||||
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
with:
|
||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||
add-paths: freqtrade/exchange/binance_leverage_tiers.json
|
||||
|
||||
+33
-33
@@ -16,8 +16,8 @@ on:
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}"
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
repository-projects: read
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: "Tests and Linting"
|
||||
@@ -25,20 +25,20 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ "ubuntu-22.04", "ubuntu-24.04", "macos-14", "macos-15" , "windows-2022", "windows-2025" ]
|
||||
python-version: ["3.11", "3.12", "3.13"]
|
||||
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
run: |
|
||||
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
|
||||
|
||||
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
@@ -87,12 +87,12 @@ jobs:
|
||||
rm -rf codecov codecov.SHA256SUM codecov.SHA256SUM.sig
|
||||
|
||||
- name: Run json schema extract
|
||||
# This should be kept before the repository check to ensure that the schema is up-to-date
|
||||
# This must be kept before the repository check to ensure that the schema is up-to-date
|
||||
run: |
|
||||
python build_helpers/extract_config_json_schema.py
|
||||
|
||||
- name: Run command docs partials extract
|
||||
# This should be kept before the repository check to ensure that the docs are up-to-date
|
||||
# This must be kept before the repository check to ensure that the docs are up-to-date
|
||||
if: ${{ (matrix.python-version == '3.13') }}
|
||||
run: |
|
||||
python build_helpers/create_command_partials.py
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Check for repository changes - Windows
|
||||
if: ${{ runner.os == 'Windows' && (matrix.python-version != '3.13') }}
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
run: |
|
||||
if (git status --porcelain) {
|
||||
Write-Host "Repository is dirty, changes detected:"
|
||||
@@ -159,6 +159,7 @@ jobs:
|
||||
shell: powershell
|
||||
run: |
|
||||
$PSVersionTable
|
||||
Get-PSRepository | Format-List *
|
||||
Set-PSRepository psgallery -InstallationPolicy trusted
|
||||
Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force -SkipPublisherCheck
|
||||
$Error.clear()
|
||||
@@ -177,12 +178,12 @@ jobs:
|
||||
name: "Mypy Version Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 #v6.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -195,11 +196,11 @@ jobs:
|
||||
name: "Pre-commit checks"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
||||
@@ -208,7 +209,7 @@ jobs:
|
||||
name: "Documentation build"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -217,7 +218,7 @@ jobs:
|
||||
./tests/test_docs.sh
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -240,17 +241,17 @@ jobs:
|
||||
name: "Tests and Linting - Online tests"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -274,6 +275,7 @@ jobs:
|
||||
|
||||
# Notify only once - when CI completes (and after deploy) in case it's successful
|
||||
notify-complete:
|
||||
name: "Notify CI Completion"
|
||||
needs: [
|
||||
build,
|
||||
build-linux-online
|
||||
@@ -281,8 +283,6 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
# Discord notification can't handle schedule events
|
||||
if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade'
|
||||
permissions:
|
||||
repository-projects: read
|
||||
steps:
|
||||
|
||||
- name: Check user permission
|
||||
@@ -320,12 +320,12 @@ jobs:
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -335,7 +335,7 @@ jobs:
|
||||
python -m build --sdist --wheel
|
||||
|
||||
- name: Upload artifacts 📦
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.1.0
|
||||
with:
|
||||
name: freqtrade-build
|
||||
path: |
|
||||
@@ -348,7 +348,7 @@ jobs:
|
||||
python -m build --sdist --wheel ft_client
|
||||
|
||||
- name: Upload artifacts 📦
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.1.0
|
||||
with:
|
||||
name: freqtrade-client-build
|
||||
path: |
|
||||
@@ -364,15 +364,15 @@ jobs:
|
||||
name: testpypi
|
||||
url: https://test.pypi.org/p/freqtrade
|
||||
permissions:
|
||||
id-token: write
|
||||
id-token: write # Needed for pypa/gh-action-pypi-publish
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download artifact 📦
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
pattern: freqtrade*-build
|
||||
path: dist
|
||||
@@ -393,15 +393,15 @@ jobs:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/freqtrade
|
||||
permissions:
|
||||
id-token: write
|
||||
id-token: write # Needed for pypa/gh-action-pypi-publish
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download artifact 📦
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
pattern: freqtrade*-build
|
||||
path: dist
|
||||
@@ -419,7 +419,7 @@ jobs:
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||
uses: ./.github/workflows/docker-build.yml
|
||||
permissions:
|
||||
packages: write
|
||||
packages: write # Needed to push package versions
|
||||
contents: read
|
||||
secrets:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -433,6 +433,6 @@ jobs:
|
||||
# Only run on push, schedule, or release events
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule') && github.repository == 'freqtrade/freqtrade'
|
||||
permissions:
|
||||
packages: write
|
||||
packages: write # Needed to delete package versions
|
||||
with:
|
||||
package_name: 'freqtrade'
|
||||
|
||||
@@ -11,6 +11,9 @@ on:
|
||||
# disable permissions for all of the available permissions
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-docs:
|
||||
@@ -19,12 +22,12 @@ jobs:
|
||||
name: Deploy Docs through mike
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
@@ -17,14 +17,17 @@ concurrency:
|
||||
group: "${{ github.workflow }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: "Build and Push Devcontainer Image"
|
||||
permissions:
|
||||
packages: write
|
||||
packages: write # Needed to push package versions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Login to GitHub Container Registry
|
||||
@@ -40,3 +43,14 @@ jobs:
|
||||
imageName: ghcr.io/${{ github.repository }}-devcontainer
|
||||
cacheFrom: ghcr.io/${{ github.repository }}-devcontainer
|
||||
push: always
|
||||
|
||||
|
||||
packages-cleanup:
|
||||
name: "Docker Package Cleanup"
|
||||
uses: ./.github/workflows/packages-cleanup.yml
|
||||
# Only run on push, schedule, or release events
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule') && github.repository == 'freqtrade/freqtrade'
|
||||
permissions:
|
||||
packages: write # Needed to delete package versions
|
||||
with:
|
||||
package_name: 'freqtrade-devcontainer'
|
||||
|
||||
@@ -17,6 +17,10 @@ on:
|
||||
default: 'develop'
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -33,10 +37,21 @@ jobs:
|
||||
if: github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Visualize disk usage before build
|
||||
run: df -h
|
||||
|
||||
- name: Cleanup some disk space
|
||||
run: |
|
||||
docker system prune -a --force || true
|
||||
docker builder prune -af || true
|
||||
|
||||
- name: Visualize disk usage after cleanup
|
||||
run: df -h
|
||||
|
||||
- name: Set docker tag names
|
||||
id: tags
|
||||
uses: ./.github/actions/docker-tags
|
||||
@@ -54,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f #v3.12.0
|
||||
|
||||
- name: Available platforms
|
||||
run: echo ${PLATFORMS}
|
||||
@@ -142,17 +157,20 @@ jobs:
|
||||
run: |
|
||||
docker images
|
||||
|
||||
- name: Visualize disk usage after build
|
||||
run: df -h
|
||||
|
||||
deploy-arm:
|
||||
name: "Deploy Docker ARM64"
|
||||
permissions:
|
||||
packages: write
|
||||
packages: write # Needed to push package versions
|
||||
needs: [ deploy-docker ]
|
||||
# Only run on 64bit machines
|
||||
runs-on: [self-hosted, linux, ARM64]
|
||||
if: github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -276,6 +294,7 @@ jobs:
|
||||
docker buildx imagetools create \
|
||||
--tag ${GHCR_IMAGE_NAME}:${TAG} \
|
||||
--tag ${GHCR_IMAGE_NAME}:latest \
|
||||
--tag ${IMAGE_NAME}:latest \
|
||||
${IMAGE_NAME}:${TAG}
|
||||
|
||||
- name: Docker images
|
||||
|
||||
@@ -4,14 +4,19 @@ on:
|
||||
branches:
|
||||
- stable
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# disable permissions for all of the available permissions
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
dockerHubDescription:
|
||||
name: "Update Docker Hub Description"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -25,20 +25,26 @@ on:
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
PACKAGE_NAME: "freqtrade"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
deploy-docker:
|
||||
name: "Delete Packages"
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.repository == 'freqtrade/freqtrade'
|
||||
permissions:
|
||||
packages: write
|
||||
packages: write # Needed to delete package versions
|
||||
|
||||
steps:
|
||||
- name: "Delete untagged Package Versions"
|
||||
uses: actions/delete-package-versions@v5
|
||||
uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0
|
||||
with:
|
||||
package-name: ${{ inputs.package_name || env.PACKAGE_NAME }}
|
||||
package-type: 'container'
|
||||
|
||||
@@ -9,15 +9,20 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
auto-update:
|
||||
name: Auto-update pre-commit hooks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -28,7 +33,7 @@ jobs:
|
||||
- name: Run auto-update
|
||||
run: pre-commit autoupdate
|
||||
|
||||
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
with:
|
||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||
add-paths: .pre-commit-config.yaml
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
name: GitHub Actions Security Analysis with zizmor 🌈
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- stable
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- stable
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Run zizmor 🌈
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
# contents: read # only needed for private repos
|
||||
# actions: read # only needed for private repos
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@e673c3917a1aef3c65c972347ed84ccd013ecda4 # v0.2.0
|
||||
@@ -0,0 +1,34 @@
|
||||
name: GitHub Actions Security Analysis with zizmor 🌈
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- stable
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- stable
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Run zizmor 🌈
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||
# contents: read # Only needed for private repos. Needed to clone the repo.
|
||||
# actions: read # Only needed for private repos. Needed for upload-sarif to read workflow run info.
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1
|
||||
@@ -21,18 +21,18 @@ repos:
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.18.2"
|
||||
rev: "v1.19.1"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==6.2.0.20251022
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.32.4.20250913
|
||||
- types-requests==2.32.4.20260107
|
||||
- types-tabulate==0.9.0.20241207
|
||||
- types-python-dateutil==2.9.0.20251115
|
||||
- scipy-stubs==1.16.3.0
|
||||
- SQLAlchemy==2.0.44
|
||||
- scipy-stubs==1.17.0.1
|
||||
- SQLAlchemy==2.0.45
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
@@ -44,7 +44,7 @@ repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.14.6'
|
||||
rev: 'v0.14.14'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
@@ -83,6 +83,6 @@ repos:
|
||||
|
||||
# Ensure github actions remain safe
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.16.3
|
||||
rev: v1.22.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
+5
-7
@@ -1,4 +1,4 @@
|
||||
FROM python:3.13.8-slim-bookworm AS base
|
||||
FROM python:3.13.11-slim-trixie AS base
|
||||
|
||||
# Setup env
|
||||
ENV LANG=C.UTF-8
|
||||
@@ -16,7 +16,8 @@ RUN mkdir /freqtrade \
|
||||
&& useradd -u 1000 -G sudo -U -m -s /bin/bash ftuser \
|
||||
&& chown ftuser:ftuser /freqtrade \
|
||||
# Allow sudoers
|
||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers
|
||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \
|
||||
&& pip install --upgrade pip
|
||||
|
||||
WORKDIR /freqtrade
|
||||
|
||||
@@ -24,14 +25,11 @@ WORKDIR /freqtrade
|
||||
FROM base AS python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
&& pip install --upgrade pip wheel
|
||||
|
||||
&& apt-get clean
|
||||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
|
||||
USER ftuser
|
||||
RUN pip install --user --no-cache-dir "numpy<3.0" \
|
||||
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt
|
||||
RUN pip install --user --no-cache-dir -r requirements-hyperopt.txt
|
||||
|
||||
# Copy dependencies to runtime-image
|
||||
FROM base AS runtime-image
|
||||
|
||||
@@ -15,7 +15,7 @@ This software is for educational purposes only. Do not risk money which
|
||||
you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
|
||||
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
|
||||
|
||||
Always start by running a trading bot in Dry-run and do not engage money
|
||||
Always start by running a trading bot in Dry-Run and do not engage money
|
||||
before you understand how it works and what profit/loss you should
|
||||
expect.
|
||||
|
||||
@@ -24,7 +24,9 @@ hesitate to read the source code and understand the mechanism of this bot.
|
||||
|
||||
## Supported Exchange marketplaces
|
||||
|
||||
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||
Please read the [exchange-specific notes](docs/exchanges.md) to learn about special configurations that maybe needed for each exchange.
|
||||
|
||||
### Supported Spot Exchanges
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [BingX](https://bingx.com/invite/0EM9RX)
|
||||
@@ -39,7 +41,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
|
||||
- [X] [MyOKX](https://okx.com/) (OKX EEA)
|
||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||
|
||||
### Supported Futures Exchanges (experimental)
|
||||
### Supported Futures Exchanges
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Bitget](https://www.bitget.com/)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
import subprocess # noqa: S404, RUF100
|
||||
import sys
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -8,7 +10,20 @@ def _write_partial_file(filename: str, content: str):
|
||||
f.write(f"``` output\n{content}\n```\n")
|
||||
|
||||
|
||||
def _get_help_output(parser) -> str:
|
||||
"""Capture the help output from a parser."""
|
||||
output = StringIO()
|
||||
parser.print_help(file=output)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def extract_command_partials():
|
||||
# Set terminal width to 80 columns for consistent output formatting
|
||||
os.environ["COLUMNS"] = "80"
|
||||
|
||||
# Import Arguments here to avoid circular imports and ensure COLUMNS is set
|
||||
from freqtrade.commands.arguments import Arguments
|
||||
|
||||
subcommands = [
|
||||
"trade",
|
||||
"create-userdir",
|
||||
@@ -46,16 +61,35 @@ def extract_command_partials():
|
||||
"recursive-analysis",
|
||||
]
|
||||
|
||||
result = subprocess.run(["freqtrade", "--help"], capture_output=True, text=True)
|
||||
# Build the Arguments class to get the parser with all subcommands
|
||||
args = Arguments(None)
|
||||
args._build_subcommands()
|
||||
|
||||
_write_partial_file("docs/commands/main.md", result.stdout)
|
||||
# Get main help output
|
||||
main_help = _get_help_output(args.parser)
|
||||
_write_partial_file("docs/commands/main.md", main_help)
|
||||
|
||||
# Get subparsers from the main parser
|
||||
# The subparsers are stored in _subparsers._group_actions[0].choices
|
||||
subparsers_action = None
|
||||
for action in args.parser._subparsers._group_actions:
|
||||
if hasattr(action, "choices"):
|
||||
subparsers_action = action
|
||||
break
|
||||
|
||||
if subparsers_action is None:
|
||||
raise RuntimeError("Could not find subparsers in the main parser")
|
||||
|
||||
for command in subcommands:
|
||||
print(f"Running for {command}")
|
||||
result = subprocess.run(["freqtrade", command, "--help"], capture_output=True, text=True)
|
||||
|
||||
_write_partial_file(f"docs/commands/{command}.md", result.stdout)
|
||||
if command in subparsers_action.choices:
|
||||
subparser = subparsers_action.choices[command]
|
||||
help_output = _get_help_output(subparser)
|
||||
_write_partial_file(f"docs/commands/{command}.md", help_output)
|
||||
else:
|
||||
print(f" Warning: subcommand '{command}' not found in parser")
|
||||
|
||||
# freqtrade-client still uses subprocess as requested
|
||||
print("Running for freqtrade-client")
|
||||
result_client = subprocess.run(["freqtrade-client", "--show"], capture_output=True, text=True)
|
||||
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11.13-slim-bookworm AS base
|
||||
FROM python:3.13.11-slim-trixie AS base
|
||||
|
||||
# Setup env
|
||||
ENV LANG=C.UTF-8
|
||||
@@ -13,7 +13,7 @@ RUN mkdir /freqtrade \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install sudo libatlas3-base libopenblas-dev curl sqlite3 libutf8proc-dev libsnappy-dev \
|
||||
&& apt-get clean \
|
||||
&& useradd -u 1000 -G sudo -U -m ftuser \
|
||||
&& useradd -u 1000 -G sudo -U -m -s /bin/bash ftuser \
|
||||
&& chown ftuser:ftuser /freqtrade \
|
||||
# Allow sudoers
|
||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \
|
||||
@@ -24,12 +24,12 @@ WORKDIR /freqtrade
|
||||
# Install dependencies
|
||||
FROM base AS python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf
|
||||
|
||||
# Install TA-lib
|
||||
COPY build_helpers/* /tmp/
|
||||
COPY build_helpers/*.whl /tmp/
|
||||
|
||||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
||||
|
||||
@@ -41,7 +41,7 @@ ranging from the simplest (0) to the most detailed per pair, per buy and per sel
|
||||
* 1: profit summaries grouped by enter_tag
|
||||
* 2: profit summaries grouped by enter_tag and exit_tag
|
||||
* 3: profit summaries grouped by pair and enter_tag
|
||||
* 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
||||
* 4: profit summaries grouped by pair, enter_tag and exit_tag (this can get quite large)
|
||||
* 5: profit summaries grouped by exit_tag
|
||||
|
||||
More options are available by running with the `-h` option.
|
||||
@@ -52,11 +52,10 @@ By default, `backtesting-analysis` processes the most recent backtest results in
|
||||
If you want to analyze results from an earlier backtest, use the `--backtest-filename` option to specify the desired file. This lets you revisit and re-analyze historical backtest outputs at any time by providing the filename of the relevant backtest result:
|
||||
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange <timerange> --export signals --backtest-filename backtest-result-2025-03-05_20-38-34.zip
|
||||
freqtrade backtesting -c <config.json> --strategy <strategy_name> --timerange <timerange> --export signals --backtest-filename backtest-result-2025-03-05_20-38-34.zip
|
||||
```
|
||||
|
||||
You should see some output similar to below in the logs with the name of the timestamped
|
||||
filename that was exported:
|
||||
You should see some output similar to below in the logs with the name of the timestamped filename that was exported:
|
||||
|
||||
```
|
||||
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "mystrat_backtest-2022-06-14_16-28-32.json"
|
||||
@@ -64,14 +63,14 @@ filename that was exported:
|
||||
|
||||
You can then use that filename in `backtesting-analysis`:
|
||||
|
||||
```
|
||||
freqtrade backtesting-analysis -c <config.json> --backtest-filename=mystrat_backtest-2022-06-14_16-28-32.json
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --backtest-filename=backtest-result-2025-03-05_20-38-34.zip
|
||||
```
|
||||
|
||||
To use a result from a different results directory, you can use `--backtest-directory` to specify the directory
|
||||
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --backtest-directory custom_results/ --backtest-filename mystrat_backtest-2022-06-14_16-28-32.json
|
||||
freqtrade backtesting-analysis -c <config.json> --backtest-directory custom_results/ --backtest-filename backtest-result-2025-03-05_20-38-34.zip
|
||||
```
|
||||
|
||||
### Tuning the buy tags and sell tags to display
|
||||
@@ -85,7 +84,7 @@ To show only certain buy and sell tags in the displayed output, use the followin
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss
|
||||
```
|
||||
|
||||
@@ -96,7 +95,7 @@ values present on signal candles to allow fine-grained investigation and tuning
|
||||
indicators. To print out a column for a given set of indicators, use the `--indicator-list`
|
||||
option:
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss --indicator-list rsi rsi_1h bb_lowerband ema_9 macd macdsignal
|
||||
```
|
||||
|
||||
@@ -108,24 +107,24 @@ output.
|
||||
The indicator values will be displayed for both entry and exit points. If `--indicator-list all` is specified,
|
||||
only the indicators at the entry point will be shown to avoid excessively large lists, which could occur depending on the strategy.
|
||||
|
||||
There are a range of candle and trade-related fields that are included in the analysis so are
|
||||
There are a range of candle and trade-related fields that are included in the analysis so are
|
||||
automatically accessible by including them on the indicator-list, and these include:
|
||||
|
||||
- **open_date :** trade open datetime
|
||||
- **close_date :** trade close datetime
|
||||
- **min_rate :** minimum price seen throughout the position
|
||||
- **max_rate :** maximum price seen throughout the position
|
||||
- **open :** signal candle open price
|
||||
- **close :** signal candle close price
|
||||
- **high :** signal candle high price
|
||||
- **low :** signal candle low price
|
||||
- **volume :** signal candle volume
|
||||
- **profit_ratio :** trade profit ratio
|
||||
- **profit_abs :** absolute profit return of the trade
|
||||
* **open_date :** trade open datetime
|
||||
* **close_date :** trade close datetime
|
||||
* **min_rate :** minimum price seen throughout the position
|
||||
* **max_rate :** maximum price seen throughout the position
|
||||
* **open :** signal candle open price
|
||||
* **close :** signal candle close price
|
||||
* **high :** signal candle high price
|
||||
* **low :** signal candle low price
|
||||
* **volume :** signal candle volume
|
||||
* **profit_ratio :** trade profit ratio
|
||||
* **profit_abs :** absolute profit return of the trade
|
||||
|
||||
#### Sample Output for Indicator Values
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen
|
||||
```
|
||||
|
||||
@@ -158,13 +157,13 @@ The `--indicator-list` option, by default, displays indicator values for both en
|
||||
|
||||
Example: Display indicator values at entry signals:
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen --entry-only
|
||||
```
|
||||
|
||||
Example: Display indicator values at exit signals:
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen --exit-only
|
||||
```
|
||||
|
||||
@@ -181,7 +180,7 @@ To show only trades between dates within your backtested timerange, supply the u
|
||||
|
||||
For example, if your backtest timerange was `20220101-20221231` but you only want to output trades in January:
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
||||
```
|
||||
|
||||
@@ -189,7 +188,7 @@ freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
||||
|
||||
Use the `--rejected-signals` option to print out rejected signals.
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --rejected-signals
|
||||
```
|
||||
|
||||
@@ -198,13 +197,13 @@ freqtrade backtesting-analysis -c <config.json> --rejected-signals
|
||||
Some of the tabular outputs can become large, so printing them out to the terminal is not preferable.
|
||||
Use the `--analysis-to-csv` option to disable printing out of tables to standard out and write them to CSV files.
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv
|
||||
```
|
||||
|
||||
By default this will write one file per output table you specified in the `backtesting-analysis` command, e.g.
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --rejected-signals --analysis-groups 0 1
|
||||
```
|
||||
|
||||
@@ -216,6 +215,6 @@ This will write to `user_data/backtest_results`:
|
||||
|
||||
To override where the files will be written, also specify the `--analysis-csv-path` option.
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --analysis-csv-path another/data/path/
|
||||
```
|
||||
|
||||
@@ -133,7 +133,7 @@ class MyAwesomeStrategy(IStrategy):
|
||||
]
|
||||
|
||||
# Define a custom max_open_trades space
|
||||
def max_open_trades_space(self) -> List[Dimension]:
|
||||
def max_open_trades_space() -> List[Dimension]:
|
||||
return [
|
||||
Integer(-1, 10, name='max_open_trades'),
|
||||
]
|
||||
@@ -142,7 +142,7 @@ class MyAwesomeStrategy(IStrategy):
|
||||
!!! Note
|
||||
All overrides are optional and can be mixed/matched as necessary.
|
||||
|
||||
### Dynamic parameters
|
||||
## Dynamic parameters
|
||||
|
||||
Parameters can also be defined dynamically, but must be available to the instance once the [`bot_start()` callback](strategy-callbacks.md#bot-start) has been called.
|
||||
|
||||
@@ -159,7 +159,7 @@ class MyAwesomeStrategy(IStrategy):
|
||||
!!! Warning
|
||||
Parameters created this way will not show up in the `list-strategies` parameter count.
|
||||
|
||||
### Overriding Base estimator
|
||||
## Overriding Base estimator
|
||||
|
||||
You can define your own optuna sampler for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass.
|
||||
|
||||
@@ -208,7 +208,6 @@ Some research will be necessary to find additional Samplers (from optunahub) for
|
||||
|
||||
Obviously the same approach will work for all other Samplers optuna supports.
|
||||
|
||||
|
||||
## Space options
|
||||
|
||||
For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types:
|
||||
|
||||
@@ -43,7 +43,9 @@ options:
|
||||
separated.
|
||||
--eps, --enable-position-stacking
|
||||
Allow buying the same pair multiple times (position
|
||||
stacking).
|
||||
stacking). Only applicable to backtesting and
|
||||
hyperopt. Results archived by this cannot be
|
||||
reproduced in dry/live trading.
|
||||
--enable-protections, --enableprotections
|
||||
Enable protections for backtesting. Will slow
|
||||
backtesting down by a considerable amount, but will
|
||||
|
||||
@@ -11,6 +11,7 @@ usage: freqtrade download-data [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[--data-format-ohlcv {json,jsongz,feather,parquet}]
|
||||
[--data-format-trades {json,jsongz,feather,parquet}]
|
||||
[--trading-mode {spot,margin,futures}]
|
||||
[--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]]
|
||||
[--prepend]
|
||||
|
||||
options:
|
||||
@@ -50,6 +51,11 @@ options:
|
||||
`feather`).
|
||||
--trading-mode, --tradingmode {spot,margin,futures}
|
||||
Select Trading mode
|
||||
--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]
|
||||
Select candle type to download. Defaults to the
|
||||
necessary candles for the selected trading mode (e.g.
|
||||
'spot' or ('futures', 'funding_rate' and 'mark') for
|
||||
futures).
|
||||
--prepend Allow data prepending. (Data-appending is disabled)
|
||||
|
||||
Common arguments:
|
||||
|
||||
@@ -41,7 +41,9 @@ options:
|
||||
functions.
|
||||
--eps, --enable-position-stacking
|
||||
Allow buying the same pair multiple times (position
|
||||
stacking).
|
||||
stacking). Only applicable to backtesting and
|
||||
hyperopt. Results archived by this cannot be
|
||||
reproduced in dry/live trading.
|
||||
--enable-protections, --enableprotections
|
||||
Enable protections for backtesting. Will slow
|
||||
backtesting down by a considerable amount, but will
|
||||
|
||||
@@ -60,6 +60,7 @@ freqtrade download-data --exchange binance --pairs ".*/USDT"
|
||||
* Given starting points are ignored if data is already available, downloading only missing data up to today.
|
||||
* Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
||||
* To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
||||
* When downloading futures data (`--trading-mode futures` or a configuration specifying futures mode), freqtrade will automatically download the necessary candle types (e.g. `mark` and `funding_rate` candles) unless specified otherwise via `--candle-types`.
|
||||
|
||||
??? Note "Permission denied errors"
|
||||
If your configuration directory `user_data` was made by docker, you may get the following error:
|
||||
|
||||
@@ -98,3 +98,50 @@ Please use configuration based [log setup](advanced-setup.md#advanced-logging) i
|
||||
|
||||
The edge module has been deprecated in 2023.9 and removed in 2025.6.
|
||||
All functionalities of edge have been removed, and having edge configured will result in an error.
|
||||
|
||||
## Adjustment to dynamic funding rate handling
|
||||
|
||||
With version 2025.12, the handling of dynamic funding rates has been adjusted to also support dynamic funding rates down to 1h funding intervals.
|
||||
As a consequence, the mark and funding rate timeframes have been changed to 1h for every supported futures exchange.
|
||||
|
||||
As the timeframe for both mark and funding_fee candles has changed (usually from 8h to 1h) - already downloaded data will have to be adjusted or partially re-downloaded.
|
||||
You can either re-download everything (`freqtrade download-data [...] --erase` - :warning: can take a long time) - or download the updated data selectively.
|
||||
|
||||
### Strategy
|
||||
|
||||
Most strategies should not need adjustments to continue to work as expected - however, strategies using `@informative("8h", candle_type="funding_rate")` or similar will have to switch the timeframe to 1h.
|
||||
The same is true for `dp.get_pair_dataframe(metadata["pair"], "8h", candle_type="funding_rate")` - which will need to be switched to 1h.
|
||||
|
||||
freqtrade will auto-adjust the timeframe and return `funding_rates` despite the wrongly given timeframe. It'll issue a warning - and may still break your strategy.
|
||||
|
||||
### Selective data re-download
|
||||
|
||||
The script below should serve as an example - you may need to adjust the timeframe and exchange to your needs!
|
||||
|
||||
``` bash
|
||||
# Cleanup no longer needed data
|
||||
rm user_data/data/<exchange>/futures/*-mark*
|
||||
rm user_data/data/<exchange>/futures/*-funding_rate*
|
||||
|
||||
# download new data (only required once to fix the mark and funding fee data)
|
||||
freqtrade download-data -t 1h --trading-mode futures --candle-types funding_rate mark [...] --timerange <full timerange you've got other data for>
|
||||
|
||||
```
|
||||
|
||||
The result of the above will be that your funding_rates and mark data will have the 1h timeframe.
|
||||
you can verify this with `freqtrade list-data --exchange <yourexchange> --show`.
|
||||
|
||||
!!! Note "Additional arguments"
|
||||
Additional arguments to the above commands may be necessary, like configuration files or explicit user_data if they deviate from the default.
|
||||
|
||||
**Hyperliquid** is a special case now - which will no longer require 1h mark data - but will use regular candles instead (this data never existed and is identical to 1h futures candles). As we don't support download-data for hyperliquid (they don't provide historic data) - there won't be actions necessary for hyperliquid users.
|
||||
|
||||
## Catboost models in freqAI
|
||||
|
||||
CatBoost models have been removed with version 2025.12 and are no longer actively supported.
|
||||
If you have existing bots using CatBoost models, you can still use them in your custom models by copy/pasting them from the git history (as linked below) and installing the Catboost library manually.
|
||||
We do however recommend switching to other supported model libraries like LightGBM or XGBoost for better support and future compatibility.
|
||||
|
||||
* [CatboostRegressor](https://github.com/freqtrade/freqtrade/blob/c6f3b0081927e161a16b116cc47fb663f7831d30/freqtrade/freqai/prediction_models/CatboostRegressor.py)
|
||||
* [CatboostClassifier](https://github.com/freqtrade/freqtrade/blob/c6f3b0081927e161a16b116cc47fb663f7831d30/freqtrade/freqai/prediction_models/CatboostClassifier.py)
|
||||
* [CatboostClassifierMultiTarget](https://github.com/freqtrade/freqtrade/blob/c6f3b0081927e161a16b116cc47fb663f7831d30/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py)
|
||||
|
||||
+2
-3
@@ -432,7 +432,6 @@ freqtrade download-data --timerange 20250625-20250801 --config tests/testdata/co
|
||||
freqtrade backtesting --config tests/testdata/config.tests.usdt.json -s SampleStrategy --userdir user_data_bttest/ --cache none --timerange 20250701-20250801
|
||||
```
|
||||
|
||||
|
||||
## Continuous integration
|
||||
|
||||
This documents some decisions taken for the CI Pipeline.
|
||||
@@ -464,10 +463,10 @@ git checkout -b new_release <commitid>
|
||||
Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these.
|
||||
|
||||
* Merge the release branch (stable) into this branch.
|
||||
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi.
|
||||
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2025.7` for July 2025). Minor versions can be `2025.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi.
|
||||
* Commit this part.
|
||||
* Push that branch to the remote and create a PR against the **stable branch**.
|
||||
* Update develop version to next version following the pattern `2019.8-dev`.
|
||||
* Update develop version to next version following the pattern `2025.8-dev`.
|
||||
|
||||
### Create changelog from git commits
|
||||
|
||||
|
||||
@@ -428,6 +428,36 @@ Your balance and trades will now be used from your vault / subaccount - and no l
|
||||
|
||||
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.
|
||||
|
||||
### HIP-3 DEXes
|
||||
|
||||
Hyperliquid supports HIP-3 decentralized exchanges (DEXes), which are independent exchanges built on top of the Hyperliquid infrastructure.
|
||||
These DEXes operate similarly to the main Hyperliquid exchange but are community-created and managed.
|
||||
|
||||
To trade on HIP-3 DEXes with Freqtrade, you need to add them to your configuration using the `hip3_dexes` parameter:
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "hyperliquid",
|
||||
"walletAddress": "your_master_wallet_address",
|
||||
"privateKey": "your_api_private_key",
|
||||
"hip3_dexes": ["dex_name_1", "dex_name_2"]
|
||||
}
|
||||
```
|
||||
|
||||
Replace `"dex_name_1"` and `"dex_name_2"` with the actual names of the HIP-3 DEXes you want to trade on (e.g. `vntl` and `xyz`).
|
||||
|
||||
!!! Warning "Performance and Rate Limit Impact"
|
||||
Each HIP-3 DEX you add significantly impacts bot performance and rate limits.
|
||||
|
||||
* **Additional API Calls**: For each HIP-3 DEX configured, Freqtrade needs to make additional API calls.
|
||||
* **Rate Limit Pressure**: Additional API calls contribute to Hyperliquid's strict rate limits. With multiple DEXes, you may hit rate limits faster, or rather, slow down bot operations due to enforced delays.
|
||||
|
||||
Please only add HIP-3 DEXes that you actively trade on. Monitor your logs for rate limit warnings or signs of slowed operations, and adjust your configuration accordingly.
|
||||
Different HIP-3 DEXes may also use different quote currencies - so make sure to only add DEXes that are compatible with your stake currency to avoid unnecessary delays.
|
||||
|
||||
!!! 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.
|
||||
|
||||
## Bitvavo
|
||||
|
||||
If your account is required to use an operatorId, you can set it in the configuration file as follows:
|
||||
|
||||
+8
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
## Supported Markets
|
||||
|
||||
Freqtrade supports spot trading, as well as (isolated) futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an up-to-date list of supported exchanges.
|
||||
Freqtrade supports spot trading, as well as futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an up-to-date list of supported exchanges.
|
||||
|
||||
### Can my bot open short positions?
|
||||
|
||||
@@ -29,6 +29,13 @@ You can however use the [`adjust_trade_position()` callback](strategy-callbacks.
|
||||
|
||||
Backtesting provides an option for this in `--eps` - however this is only there to highlight "hidden" signals, and will not work in live.
|
||||
|
||||
### Does freqtrade support sandbox accounts?
|
||||
|
||||
No, but you can use dry-run mode to simulate trading without risking real funds.
|
||||
|
||||
Sandbox markets are separate, simulated markets - which are not suitable to test your strategy in a realistic environment.
|
||||
These markets usually have different order books, liquidity and trading behaviour (usually with very few participants) - which makes them unsuitable for realistic tests of your strategy.
|
||||
|
||||
### The bot does not start
|
||||
|
||||
Running the bot with `freqtrade trade --config config.json` shows the output `freqtrade: command not found`.
|
||||
|
||||
@@ -200,15 +200,15 @@ If this value is set, FreqAI will initially use the predictions from the trainin
|
||||
|
||||
## Using different prediction models
|
||||
|
||||
FreqAI has multiple example prediction model libraries that are ready to be used as is via the flag `--freqaimodel`. These libraries include `CatBoost`, `LightGBM`, and `XGBoost` regression, classification, and multi-target models, and can be found in `freqai/prediction_models/`.
|
||||
FreqAI has multiple example prediction model libraries that are ready to be used as is via the flag `--freqaimodel`. These libraries include `LightGBM`, and `XGBoost` regression, classification, and multi-target models, and can be found in `freqai/prediction_models/`.
|
||||
|
||||
Regression and classification models differ in what targets they predict - a regression model will predict a target of continuous values, for example what price BTC will be at tomorrow, whilst a classifier will predict a target of discrete values, for example if the price of BTC will go up tomorrow or not. This means that you have to specify your targets differently depending on which model type you are using (see details [below](#setting-model-targets)).
|
||||
|
||||
All of the aforementioned model libraries implement gradient boosted decision tree algorithms. They all work on the principle of ensemble learning, where predictions from multiple simple learners are combined to get a final prediction that is more stable and generalized. The simple learners in this case are decision trees. Gradient boosting refers to the method of learning, where each simple learner is built in sequence - the subsequent learner is used to improve on the error from the previous learner. If you want to learn more about the different model libraries you can find the information in their respective docs:
|
||||
|
||||
* CatBoost: https://catboost.ai/en/docs/
|
||||
* LightGBM: https://lightgbm.readthedocs.io/en/v3.3.2/#
|
||||
* XGBoost: https://xgboost.readthedocs.io/en/stable/#
|
||||
* LightGBM: <https://lightgbm.readthedocs.io/en/v3.3.2/#>
|
||||
* XGBoost: <https://xgboost.readthedocs.io/en/stable/#>
|
||||
* CatBoost: <https://catboost.ai/en/docs/> (No longer actively supported since 2025.12)
|
||||
|
||||
There are also numerous online articles describing and comparing the algorithms. Some relatively lightweight examples would be [CatBoost vs. LightGBM vs. XGBoost — Which is the best algorithm?](https://towardsdatascience.com/catboost-vs-lightgbm-vs-xgboost-c80f40662924#:~:text=In%20CatBoost%2C%20symmetric%20trees%2C%20or,the%20same%20depth%20can%20differ.) and [XGBoost, LightGBM or CatBoost — which boosting algorithm should I use?](https://medium.com/riskified-technology/xgboost-lightgbm-or-catboost-which-boosting-algorithm-should-i-use-e7fda7bb36bc). Keep in mind that the performance of each model is highly dependent on the application and so any reported metrics might not be true for your particular use of the model.
|
||||
|
||||
@@ -219,7 +219,7 @@ Make sure to use unique names to avoid overriding built-in models.
|
||||
|
||||
#### Regressors
|
||||
|
||||
If you are using a regressor, you need to specify a target that has continuous values. FreqAI includes a variety of regressors, such as the `CatboostRegressor`via the flag `--freqaimodel CatboostRegressor`. An example of how you could set a regression target for predicting the price 100 candles into the future would be
|
||||
If you are using a regressor, you need to specify a target that has continuous values. FreqAI includes a variety of regressors, such as the `LightGBMRegressor`via the flag `--freqaimodel LightGBMRegressor`. An example of how you could set a regression target for predicting the price 100 candles into the future would be
|
||||
|
||||
```python
|
||||
df['&s-close_price'] = df['close'].shift(-100)
|
||||
@@ -229,7 +229,7 @@ If you want to predict multiple targets, you need to define multiple labels usin
|
||||
|
||||
#### Classifiers
|
||||
|
||||
If you are using a classifier, you need to specify a target that has discrete values. FreqAI includes a variety of classifiers, such as the `CatboostClassifier` via the flag `--freqaimodel CatboostClassifier`. If you elects to use a classifier, the classes need to be set using strings. For example, if you want to predict if the price 100 candles into the future goes up or down you would set
|
||||
If you are using a classifier, you need to specify a target that has discrete values. FreqAI includes a variety of classifiers, such as the `LightGBMClassifier` via the flag `--freqaimodel LightGBMClassifier`. If you elects to use a classifier, the classes need to be set using strings. For example, if you want to predict if the price 100 candles into the future goes up or down you would set
|
||||
|
||||
```python
|
||||
df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down')
|
||||
|
||||
@@ -107,7 +107,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
||||
| `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`.
|
||||
|
||||
|
||||
### Additional parameters
|
||||
|
||||
| Parameter | Description |
|
||||
@@ -116,3 +115,4 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
||||
| `freqai.keras` | If the selected model makes use of Keras (typical for TensorFlow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
| `freqai.conv_width` | The width of a neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
||||
| `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
| `freqai.override_exchange_check` | Override the exchange check to force FreqAI to use exchanges that may not have enough historic data. Turn this to True if you know your FreqAI model and strategy do not require historical data. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
## Highlighted changes
|
||||
# Highlighted changes
|
||||
|
||||
- ...
|
||||
|
||||
### How to update
|
||||
## How to update
|
||||
|
||||
As always, you can update your bot using one of the following commands:
|
||||
|
||||
#### docker-compose
|
||||
### docker-compose
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### Installation via setup script
|
||||
### Installation via setup script
|
||||
|
||||
```
|
||||
``` bash
|
||||
# Deactivate venv and run
|
||||
./setup.sh --update
|
||||
```
|
||||
|
||||
#### Plain native installation
|
||||
### Plain native installation
|
||||
|
||||
```
|
||||
``` bash
|
||||
git pull
|
||||
pip install -U -r requirements.txt
|
||||
```
|
||||
|
||||
+3
-1
@@ -37,6 +37,8 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
||||
|
||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||
|
||||
### Supported Spot Exchanges
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [BingX](https://bingx.com/invite/0EM9RX)
|
||||
- [X] [Bitget](https://www.bitget.com/)
|
||||
@@ -50,7 +52,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
||||
- [X] [MyOKX](https://okx.com/) (OKX EEA)
|
||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||
|
||||
### Supported Futures Exchanges (experimental)
|
||||
### Supported Futures Exchanges
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Bitget](https://www.bitget.com/)
|
||||
|
||||
+57
-18
@@ -9,15 +9,25 @@ The freqtrade documentation describes various ways to install freqtrade
|
||||
* [Manual Installation](#manual-installation)
|
||||
* [Installation with Conda](#installation-with-conda)
|
||||
|
||||
Please consider using the prebuilt [docker images](docker_quickstart.md) to get started quickly while evaluating how freqtrade works.
|
||||
Please consider using the prebuilt [docker images](docker_quickstart.md) to get started quickly.
|
||||
|
||||
!!! Note "Updating"
|
||||
Keeping freqtrade updated is important to [ensure ongoing compatibility](updating.md#why-update) with exchange API's.
|
||||
Please refer to the [updating guide](updating.md) for details on how to update your installation.
|
||||
|
||||
!!! Note "Windows users"
|
||||
We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure).
|
||||
|
||||
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu/Linux instructions will work.
|
||||
If you really want to install freqtrade natively on Windows, best use the [`./setup.ps1` installation script](#use-setupps1-windows).
|
||||
|
||||
Please also make sure to use the 64bit version of Python, as 32bit versions have severe memory limitations, which can negatively impact your experience with backtesting/hyperopt.
|
||||
|
||||
------
|
||||
|
||||
## Information
|
||||
|
||||
For Windows installation, please use the [windows installation guide](windows_installation.md).
|
||||
|
||||
The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the `./setup.sh` script, if it's available for your platform.
|
||||
The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the `./setup.sh` (`./setup.ps1` for Windows) script, if it's available for your platform.
|
||||
|
||||
!!! Note "Version considerations"
|
||||
When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests).
|
||||
@@ -152,20 +162,9 @@ If you are on Debian, Ubuntu or MacOS, freqtrade provides the script to install
|
||||
./setup.sh -i
|
||||
```
|
||||
|
||||
### Activate your virtual environment
|
||||
#### Other options of /setup.sh script
|
||||
|
||||
Each time you open a new terminal, you must run `source .venv/bin/activate` to activate your virtual environment.
|
||||
|
||||
```bash
|
||||
# activate virtual environment
|
||||
source ./.venv/bin/activate
|
||||
```
|
||||
|
||||
[You are now ready](#you-are-ready) to run the bot.
|
||||
|
||||
### Other options of /setup.sh script
|
||||
|
||||
You can as well update, configure and reset the codebase of your bot with `./script.sh`
|
||||
You can also update, configure and reset the codebase of your bot with `./setup.sh`
|
||||
|
||||
```bash
|
||||
# --update, Command git pull to update.
|
||||
@@ -194,6 +193,34 @@ This option will pull the last version of your current branch and update your vi
|
||||
This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv.
|
||||
```
|
||||
|
||||
#### Activate your virtual environment
|
||||
|
||||
Each time you open a new terminal, you must run `source .venv/bin/activate` to activate your virtual environment.
|
||||
|
||||
```bash
|
||||
# activate virtual environment
|
||||
source ./.venv/bin/activate
|
||||
```
|
||||
|
||||
### Use ./setup.ps1 (Windows)
|
||||
|
||||
The script will ask you a few questions to determine which parts should be installed.
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -ExecutionPolicy Bypass
|
||||
cd freqtrade
|
||||
. .\setup.ps1
|
||||
```
|
||||
|
||||
#### Activate your virtual environment (Windows)
|
||||
|
||||
```powershell
|
||||
# activate virtual environment
|
||||
. .\.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
[You are now ready](#you-are-ready) to run the bot.
|
||||
|
||||
-----
|
||||
|
||||
## Manual Installation
|
||||
@@ -337,7 +364,7 @@ conda deactivate
|
||||
|
||||
Happy trading!
|
||||
|
||||
-----
|
||||
------
|
||||
|
||||
## You are ready
|
||||
|
||||
@@ -394,3 +421,15 @@ open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10
|
||||
```
|
||||
|
||||
If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details.
|
||||
|
||||
### Windows Installation error
|
||||
|
||||
```bash
|
||||
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
|
||||
```
|
||||
|
||||
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||
|
||||
You can download the Visual C++ build tools from [the Visual Studio website](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and install "Desktop development with C++" in it's default configuration. Unfortunately, this is a heavy download / dependency so you might want to consider WSL2 or [docker compose](docker_quickstart.md) first.
|
||||
|
||||

|
||||
|
||||
+1
-4
@@ -1,8 +1,5 @@
|
||||
# Trading with Leverage
|
||||
|
||||
!!! Warning "Beta feature"
|
||||
This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord or via Github Issue.
|
||||
|
||||
!!! Note "Multiple bots on one account"
|
||||
You can't run 2 bots on the same account with leverage. For leveraged / margin trading, freqtrade assumes it's the only user of the account, and all liquidation levels are calculated based on this assumption.
|
||||
|
||||
@@ -55,7 +52,7 @@ Perpetual swaps (also known as Perpetual Futures) are contracts traded at a pric
|
||||
In addition to the gains/losses from the change in price of the futures contract, traders also exchange _funding fees_, which are gains/losses worth an amount that is derived from the difference in price between the futures contract and the underlying asset. The difference in price between a futures contract and the underlying asset varies between exchanges.
|
||||
|
||||
To trade in futures markets, you'll have to set `trading_mode` to "futures".
|
||||
You will also have to pick a "margin mode" (explanation below) - with freqtrade currently only supporting isolated margin.
|
||||
You will also have to pick a "margin mode" (explanation below).
|
||||
|
||||
``` json
|
||||
"trading_mode": "futures",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
markdown==3.10
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.7.0
|
||||
mkdocs-material==9.7.1
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.17.1
|
||||
pymdown-extensions==10.20
|
||||
jinja2==3.1.6
|
||||
mike==2.1.3
|
||||
|
||||
+6
-1
@@ -31,9 +31,14 @@ The Order-type will be ignored if only one mode is available.
|
||||
--8<-- "includes/exchange-features.md"
|
||||
|
||||
!!! Note "Tight stoploss"
|
||||
<ins>Do not set too low/tight stoploss value when using stop loss on exchange!</ins>
|
||||
Do not set too low/tight stoploss value when using stop loss on exchange!
|
||||
If set to low/tight you will have greater risk of missing fill on the order and stoploss will not work.
|
||||
|
||||
!!! Warning "Loose stoploss"
|
||||
Using stoploss on exchange with a very wide stoploss (e.g. -1) may fail to place the stoploss order on exchange due to exchange limitations.
|
||||
In that case, the bot will fallback to using the `emergency_exit` order type to place a market order as placing the stoploss order failed.
|
||||
Freqtrade currently does not implement a limitation to avoid this situation, so please ensure your stoploss values are within reasonable limits for your exchange or disable stoploss on exchange.
|
||||
|
||||
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
|
||||
|
||||
Enable or Disable stop loss on exchange.
|
||||
|
||||
@@ -634,7 +634,7 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
## Custom order price rules
|
||||
|
||||
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
||||
By default, freqtrade use the orderbook to automatically set an order price ([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
||||
|
||||
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
|
||||
|
||||
@@ -644,7 +644,7 @@ Each of these methods are called right before placing an order on the exchange.
|
||||
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
||||
|
||||
!!! Note
|
||||
Using custom_entry_price, the Trade object will be available as soon as the first entry order associated with the trade is created, for the first entry, `trade` parameter value will be `None`.
|
||||
When using `custom_entry_price()`, the Trade object will be available as soon as the first entry order associated with the trade is created, for the first entry, `trade` parameter value will be `None`.
|
||||
|
||||
### Custom order entry and exit price example
|
||||
|
||||
@@ -1292,6 +1292,22 @@ Currently two types of annotations are supported, `area` and `line`.
|
||||
}
|
||||
```
|
||||
|
||||
#### Point
|
||||
|
||||
``` json
|
||||
{
|
||||
"type": "point", // Type of the annotation, currently only "point" is supported
|
||||
"x": "2024-01-01 15:00:00", // Start date of the point
|
||||
"y": 94000.2, // Price / y axis value
|
||||
"color": "",
|
||||
"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",
|
||||
"size": 2, // Optional, line width in pixels. Defaults to 10
|
||||
"symbol": "circle", // Optional, can be "circle", "rect", "roundRect", "triangle", "pin", "arrow", "none".
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
The below example will mark the chart with areas for the hours 8 and 15, with a grey color, highlighting the market open and close hours.
|
||||
This is obviously a very basic example.
|
||||
|
||||
|
||||
+14
-3
@@ -6,6 +6,12 @@ To update your freqtrade installation, please use one of the below methods, corr
|
||||
Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release.
|
||||
For the develop branch, please follow PR's to avoid being surprised by changes.
|
||||
|
||||
## Why update?
|
||||
|
||||
Keeping your bot updated not only ensures that you have the latest features and improvements, but is a requirement to keep your bot running smoothly.
|
||||
Freqtrade is heavily dependent on the underlying exchange API's, which change pretty frequently if considered across exchanges.
|
||||
To ensure ongoing compatibility, please make sure to update your bot regularly.
|
||||
|
||||
## Docker
|
||||
|
||||
!!! Note "Legacy installations using the `master` image"
|
||||
@@ -38,7 +44,12 @@ pip install -e .
|
||||
freqtrade install-ui
|
||||
```
|
||||
|
||||
### Problems updating
|
||||
## Problems updating
|
||||
|
||||
Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from updated dependencies, which fail to install (for example TA-lib).
|
||||
Please refer to the corresponding installation sections (common problems linked below)
|
||||
Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from dependencies which fail to install.
|
||||
We try to make sure that heavy dependencies have wheels available for major platforms, but sometimes this is not possible.
|
||||
|
||||
Please refer to the corresponding installation sections (common problem sections linked below).
|
||||
|
||||
[Common installation problems](installation.md#troubleshooting)
|
||||
[Common installation problems - windows](installation.md#windows-installation-error)
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# Windows installation
|
||||
|
||||
We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure).
|
||||
|
||||
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
||||
Otherwise, please follow the instructions below.
|
||||
|
||||
All instructions assume that python 3.11+ is installed and available.
|
||||
|
||||
## Clone the git repository
|
||||
|
||||
First of all clone the repository by running:
|
||||
|
||||
``` powershell
|
||||
git clone https://github.com/freqtrade/freqtrade.git
|
||||
```
|
||||
|
||||
Now, choose your installation method, either automatically via script (recommended) or manually following the corresponding instructions.
|
||||
|
||||
## Install freqtrade automatically
|
||||
|
||||
### Run the installation script
|
||||
|
||||
The script will ask you a few questions to determine which parts should be installed.
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -ExecutionPolicy Bypass
|
||||
cd freqtrade
|
||||
. .\setup.ps1
|
||||
```
|
||||
|
||||
## Install freqtrade manually
|
||||
|
||||
!!! Note "64bit Python version"
|
||||
Please make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows.
|
||||
32bit python versions are no longer supported under Windows.
|
||||
|
||||
!!! Hint
|
||||
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
|
||||
|
||||
|
||||
### Error during installation on Windows
|
||||
|
||||
``` bash
|
||||
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
|
||||
```
|
||||
|
||||
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||
|
||||
You can download the Visual C++ build tools from [here](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and install "Desktop development with C++" in it's default configuration. Unfortunately, this is a heavy download / dependency so you might want to consider WSL2 or [docker compose](docker_quickstart.md) first.
|
||||
|
||||

|
||||
|
||||
---
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2025.11.1"
|
||||
__version__ = "2026.2-dev"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -3,6 +3,7 @@ This module contains the argument manager class
|
||||
"""
|
||||
|
||||
from argparse import ArgumentParser, Namespace, _ArgumentGroup
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -102,7 +103,13 @@ ARGS_BACKTEST_SHOW = [
|
||||
"backtest_breakdown",
|
||||
]
|
||||
|
||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"]
|
||||
ARGS_LIST_EXCHANGES = [
|
||||
"print_one_column",
|
||||
"list_exchanges_all",
|
||||
"trading_mode",
|
||||
"dex_exchanges",
|
||||
"list_exchanges_futures_options",
|
||||
]
|
||||
|
||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column", "trading_mode"]
|
||||
|
||||
@@ -174,6 +181,7 @@ ARGS_DOWNLOAD_DATA = [
|
||||
"dataformat_ohlcv",
|
||||
"dataformat_trades",
|
||||
"trading_mode",
|
||||
"candle_types",
|
||||
"prepend_data",
|
||||
]
|
||||
|
||||
@@ -348,7 +356,11 @@ class Arguments:
|
||||
def _build_args(self, optionlist: list[str], parser: ArgumentParser | _ArgumentGroup) -> None:
|
||||
for val in optionlist:
|
||||
opt = AVAILABLE_CLI_OPTIONS[val]
|
||||
parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
|
||||
options = deepcopy(opt.kwargs)
|
||||
help_text = options.pop("help", None)
|
||||
if opt.fthelp and isinstance(opt.fthelp, dict) and hasattr(parser, "prog"):
|
||||
help_text = opt.fthelp.get(parser.prog, help_text)
|
||||
parser.add_argument(*opt.cli, dest=val, help=help_text, **options)
|
||||
|
||||
def _build_subcommands(self) -> None:
|
||||
"""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Definition of cli arguments used in arguments.py
|
||||
"""
|
||||
|
||||
from argparse import ArgumentTypeError
|
||||
from argparse import SUPPRESS, ArgumentTypeError
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.constants import (
|
||||
@@ -38,8 +38,14 @@ def check_int_nonzero(value: str) -> int:
|
||||
|
||||
class Arg:
|
||||
# Optional CLI arguments
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, fthelp: dict[str, str] | None = None, **kwargs):
|
||||
"""
|
||||
CLI Arguments - used to build subcommand parsers consistently.
|
||||
:param fthelp: dict - fthelp per command - should be "freqtrade <command>": help_text
|
||||
If not provided or not found, 'help' from kwargs is used instead.
|
||||
"""
|
||||
self.cli = args
|
||||
self.fthelp = fthelp
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
||||
@@ -174,7 +180,11 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"position_stacking": Arg(
|
||||
"--eps",
|
||||
"--enable-position-stacking",
|
||||
help="Allow buying the same pair multiple times (position stacking).",
|
||||
help=(
|
||||
"Allow buying the same pair multiple times (position stacking). "
|
||||
"Only applicable to backtesting and hyperopt. "
|
||||
"Results archived by this cannot be reproduced in dry/live trading."
|
||||
),
|
||||
action="store_true",
|
||||
default=False,
|
||||
),
|
||||
@@ -378,6 +388,13 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help="Print only DEX exchanges.",
|
||||
action="store_true",
|
||||
),
|
||||
"list_exchanges_futures_options": Arg(
|
||||
"--ccxt-show-futures-options-exchanges",
|
||||
help=SUPPRESS,
|
||||
# Show compatibility with ccxt for futures functionality
|
||||
# Doesn't show in help as it's an internal/debug option.
|
||||
action="store_true",
|
||||
),
|
||||
# List pairs / markets
|
||||
"list_pairs_all": Arg(
|
||||
"-a",
|
||||
@@ -422,6 +439,14 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"candle_types": Arg(
|
||||
"--candle-types",
|
||||
fthelp={
|
||||
"freqtrade download-data": (
|
||||
"Select candle type to download. "
|
||||
"Defaults to the necessary candles for the selected trading mode "
|
||||
"(e.g. 'spot' or ('futures', 'funding_rate' and 'mark') for futures)."
|
||||
),
|
||||
"_": "Select candle type to convert. Defaults to all available types.",
|
||||
},
|
||||
help="Select candle type to convert. Defaults to all available types.",
|
||||
choices=[c.value for c in CandleType],
|
||||
nargs="+",
|
||||
|
||||
@@ -38,13 +38,15 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
||||
else:
|
||||
available_exchanges = [e for e in available_exchanges if e["valid"] is not False]
|
||||
title = f"Exchanges available for Freqtrade ({len(available_exchanges)} exchanges):"
|
||||
|
||||
show_fut_reasons = args.get("list_exchanges_futures_options", False)
|
||||
table = Table(title=title)
|
||||
|
||||
table.add_column("Exchange Name")
|
||||
table.add_column("Class Name")
|
||||
table.add_column("Markets")
|
||||
table.add_column("Reason")
|
||||
if show_fut_reasons:
|
||||
table.add_column("Futures Reason")
|
||||
|
||||
trading_mode = args.get("trading_mode", None)
|
||||
dex_only = args.get("dex_exchanges", False)
|
||||
@@ -78,12 +80,14 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
||||
if exchange["dex"]:
|
||||
trade_modes = Text("DEX: ") + trade_modes
|
||||
trade_modes.stylize("bold", 0, 3)
|
||||
futcol = [] if not show_fut_reasons else [exchange["comment_futures"]]
|
||||
|
||||
table.add_row(
|
||||
name,
|
||||
classname,
|
||||
trade_modes,
|
||||
exchange["comment"],
|
||||
*futcol,
|
||||
style=None if exchange["valid"] else "red",
|
||||
)
|
||||
# table.add_row(*[exchange[header] for header in headers])
|
||||
|
||||
@@ -30,7 +30,7 @@ def log_config_error_range(path: str, errmsg: str) -> str:
|
||||
offsetlist = re.findall(r"(?<=Parse\serror\sat\soffset\s)\d+", errmsg)
|
||||
if offsetlist:
|
||||
offset = int(offsetlist[0])
|
||||
text = Path(path).read_text()
|
||||
text = Path(path).read_text(encoding="utf-8")
|
||||
# Fetch an offset of 80 characters around the error line
|
||||
subtext = text[offset - min(80, offset) : offset + 80]
|
||||
segments = subtext.split("\n")
|
||||
|
||||
@@ -38,7 +38,8 @@ def ohlcv_to_dataframe(
|
||||
cols = DEFAULT_DATAFRAME_COLUMNS
|
||||
df = DataFrame(ohlcv, columns=cols)
|
||||
|
||||
df["date"] = to_datetime(df["date"], unit="ms", utc=True)
|
||||
# Floor date to seconds to account for exchange imprecisions
|
||||
df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor("s")
|
||||
|
||||
# Some exchanges return int values for Volume and even for OHLC.
|
||||
# Convert them since TA-LIB indicators used in the strategy assume floats
|
||||
|
||||
@@ -348,6 +348,22 @@ class DataProvider:
|
||||
)
|
||||
return total_candles
|
||||
|
||||
def __fix_funding_rate_timeframe(
|
||||
self, pair: str, timeframe: str | None, candle_type: str
|
||||
) -> str | None:
|
||||
if (
|
||||
candle_type == CandleType.FUNDING_RATE
|
||||
and (ff_tf := self.get_funding_rate_timeframe()) != timeframe
|
||||
):
|
||||
# TODO: does this message make sense? might be pointless as funding fees don't
|
||||
# have a timeframe
|
||||
logger.warning(
|
||||
f"{pair}, {timeframe} requested - funding rate timeframe not matching {ff_tf}."
|
||||
)
|
||||
return ff_tf
|
||||
|
||||
return timeframe
|
||||
|
||||
def get_pair_dataframe(
|
||||
self, pair: str, timeframe: str | None = None, candle_type: str = ""
|
||||
) -> DataFrame:
|
||||
@@ -361,6 +377,7 @@ class DataProvider:
|
||||
:return: Dataframe for this pair
|
||||
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||
"""
|
||||
timeframe = self.__fix_funding_rate_timeframe(pair, timeframe, candle_type)
|
||||
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||
# Get live OHLCV data.
|
||||
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||
@@ -620,3 +637,12 @@ class DataProvider:
|
||||
except ExchangeError:
|
||||
logger.warning(f"Could not fetch market data for {pair}. Assuming no delisting.")
|
||||
return None
|
||||
|
||||
def get_funding_rate_timeframe(self) -> str:
|
||||
"""
|
||||
Get the funding rate timeframe from exchange options
|
||||
:return: Timeframe string
|
||||
"""
|
||||
if self._exchange is None:
|
||||
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
||||
return self._exchange.get_option("funding_fee_timeframe")
|
||||
|
||||
@@ -397,6 +397,9 @@ class IDataHandler(ABC):
|
||||
pairdf = self._ohlcv_load(
|
||||
pair, timeframe, timerange=timerange_startup, candle_type=candle_type
|
||||
)
|
||||
if not pairdf.empty and candle_type == CandleType.FUNDING_RATE:
|
||||
# Funding rate data is sometimes off by a couple of ms - floor to seconds
|
||||
pairdf["date"] = pairdf["date"].dt.floor("s")
|
||||
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
|
||||
return pairdf
|
||||
else:
|
||||
@@ -508,8 +511,15 @@ class IDataHandler(ABC):
|
||||
Applies to bybit and okx, where funding-fee and mark candles have different timeframes.
|
||||
"""
|
||||
paircombs = self.ohlcv_get_available_data(self._datadir, TradingMode.FUTURES)
|
||||
ff_timeframe_s = timeframe_to_seconds(ff_timeframe)
|
||||
|
||||
funding_rate_combs = [
|
||||
f for f in paircombs if f[2] == CandleType.FUNDING_RATE and f[1] != ff_timeframe
|
||||
f
|
||||
for f in paircombs
|
||||
if f[2] == CandleType.FUNDING_RATE
|
||||
and f[1] != ff_timeframe
|
||||
# Only allow smaller timeframes to move from smaller to larger timeframes
|
||||
and timeframe_to_seconds(f[1]) < ff_timeframe_s
|
||||
]
|
||||
|
||||
if funding_rate_combs:
|
||||
|
||||
@@ -308,11 +308,15 @@ def _download_pair_history(
|
||||
candle_type=candle_type,
|
||||
until_ms=until_ms if until_ms else None,
|
||||
)
|
||||
logger.info(f"Downloaded data for {pair} with length {len(new_dataframe)}.")
|
||||
logger.info(
|
||||
f"Downloaded data for {pair}, {timeframe}, {candle_type} with length "
|
||||
f"{len(new_dataframe)}."
|
||||
)
|
||||
else:
|
||||
new_dataframe = pair_candles
|
||||
logger.info(
|
||||
f"Downloaded data for {pair} with length {len(new_dataframe)}. Parallel Method."
|
||||
f"Downloaded data for {pair}, {timeframe}, {candle_type} with length "
|
||||
f"{len(new_dataframe)}. Parallel Method."
|
||||
)
|
||||
|
||||
if data.empty:
|
||||
@@ -349,6 +353,7 @@ def _download_pair_history(
|
||||
|
||||
def refresh_backtest_ohlcv_data(
|
||||
exchange: Exchange,
|
||||
*,
|
||||
pairs: list[str],
|
||||
timeframes: list[str],
|
||||
datadir: Path,
|
||||
@@ -359,6 +364,7 @@ def refresh_backtest_ohlcv_data(
|
||||
data_format: str | None = None,
|
||||
prepend: bool = False,
|
||||
progress_tracker: CustomProgress | None = None,
|
||||
candle_types: list[CandleType] | None = None,
|
||||
no_parallel_download: bool = False,
|
||||
) -> list[str]:
|
||||
"""
|
||||
@@ -371,10 +377,44 @@ def refresh_backtest_ohlcv_data(
|
||||
pairs_not_available = []
|
||||
fast_candles: dict[PairWithTimeframe, DataFrame] = {}
|
||||
data_handler = get_datahandler(datadir, data_format)
|
||||
candle_type = CandleType.get_default(trading_mode)
|
||||
def_candletype = CandleType.SPOT if trading_mode != "futures" else CandleType.FUTURES
|
||||
if trading_mode != "futures":
|
||||
# Ignore user passed candle types for non-futures trading
|
||||
timeframes_with_candletype = [(tf, def_candletype) for tf in timeframes]
|
||||
else:
|
||||
# Filter out SPOT candle type for futures trading
|
||||
candle_types = (
|
||||
[ct for ct in candle_types if ct != CandleType.SPOT] if candle_types else None
|
||||
)
|
||||
fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
|
||||
tf_funding_rate = exchange.get_option("funding_fee_timeframe")
|
||||
tf_mark = exchange.get_option("mark_ohlcv_timeframe")
|
||||
|
||||
if candle_types:
|
||||
for ct in candle_types:
|
||||
exchange.verify_candle_type_support(ct)
|
||||
timeframes_with_candletype = [
|
||||
(tf, ct)
|
||||
for ct in candle_types
|
||||
for tf in timeframes
|
||||
if ct != CandleType.FUNDING_RATE
|
||||
]
|
||||
else:
|
||||
# Default behavior
|
||||
timeframes_with_candletype = [(tf, def_candletype) for tf in timeframes]
|
||||
timeframes_with_candletype.append((tf_mark, fr_candle_type))
|
||||
if not candle_types or CandleType.FUNDING_RATE in candle_types:
|
||||
# All exchanges need FundingRate for futures trading.
|
||||
# The timeframe is aligned to the mark-price timeframe.
|
||||
timeframes_with_candletype.append((tf_funding_rate, CandleType.FUNDING_RATE))
|
||||
# Deduplicate list ...
|
||||
timeframes_with_candletype = list(dict.fromkeys(timeframes_with_candletype))
|
||||
logger.debug(
|
||||
"Downloading %s.", ", ".join(f'"{tf} {ct}"' for tf, ct in timeframes_with_candletype)
|
||||
)
|
||||
|
||||
with progress_tracker as progress:
|
||||
tf_length = len(timeframes) if trading_mode != "futures" else len(timeframes) + 2
|
||||
timeframe_task = progress.add_task("Timeframe", total=tf_length)
|
||||
timeframe_task = progress.add_task("Timeframe", total=len(timeframes_with_candletype))
|
||||
pair_task = progress.add_task("Downloading data...", total=len(pairs))
|
||||
|
||||
for pair in pairs:
|
||||
@@ -385,7 +425,7 @@ def refresh_backtest_ohlcv_data(
|
||||
pairs_not_available.append(f"{pair}: Pair not available on exchange.")
|
||||
logger.info(f"Skipping pair {pair}...")
|
||||
continue
|
||||
for timeframe in timeframes:
|
||||
for timeframe, candle_type in timeframes_with_candletype:
|
||||
# Get fast candles via parallel method on first loop through per timeframe
|
||||
# and candle type. Downloads all the pairs in the list and stores them.
|
||||
# Also skips if only 1 pair/timeframe combination is scheduled for download.
|
||||
@@ -412,7 +452,7 @@ def refresh_backtest_ohlcv_data(
|
||||
# get the already downloaded pair candles if they exist
|
||||
pair_candles = fast_candles.pop((pair, timeframe, candle_type), None)
|
||||
|
||||
progress.update(timeframe_task, description=f"Timeframe {timeframe}")
|
||||
progress.update(timeframe_task, description=f"Timeframe {timeframe} {candle_type}")
|
||||
logger.debug(f"Downloading pair {pair}, {candle_type}, interval {timeframe}.")
|
||||
_download_pair_history(
|
||||
pair=pair,
|
||||
@@ -428,33 +468,6 @@ def refresh_backtest_ohlcv_data(
|
||||
pair_candles=pair_candles, # optional pass of dataframe of parallel candles
|
||||
)
|
||||
progress.update(timeframe_task, advance=1)
|
||||
if trading_mode == "futures":
|
||||
# Predefined candletype (and timeframe) depending on exchange
|
||||
# Downloads what is necessary to backtest based on futures data.
|
||||
tf_mark = exchange.get_option("mark_ohlcv_timeframe")
|
||||
tf_funding_rate = exchange.get_option("funding_fee_timeframe")
|
||||
|
||||
fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
|
||||
# All exchanges need FundingRate for futures trading.
|
||||
# The timeframe is aligned to the mark-price timeframe.
|
||||
combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark))
|
||||
for candle_type_f, tf in combs:
|
||||
logger.debug(f"Downloading pair {pair}, {candle_type_f}, interval {tf}.")
|
||||
_download_pair_history(
|
||||
pair=pair,
|
||||
datadir=datadir,
|
||||
exchange=exchange,
|
||||
timerange=timerange,
|
||||
data_handler=data_handler,
|
||||
timeframe=str(tf),
|
||||
new_pairs_days=new_pairs_days,
|
||||
candle_type=candle_type_f,
|
||||
erase=erase,
|
||||
prepend=prepend,
|
||||
)
|
||||
progress.update(
|
||||
timeframe_task, advance=1, description=f"Timeframe {candle_type_f}, {tf}"
|
||||
)
|
||||
|
||||
progress.update(pair_task, advance=1)
|
||||
progress.update(timeframe_task, description="Timeframe")
|
||||
@@ -800,6 +813,7 @@ def download_data(
|
||||
trading_mode=config.get("trading_mode", "spot"),
|
||||
prepend=config.get("prepend_data", False),
|
||||
progress_tracker=progress_tracker,
|
||||
candle_types=config.get("candle_types"),
|
||||
no_parallel_download=config.get("no_parallel_download", False),
|
||||
)
|
||||
finally:
|
||||
|
||||
@@ -74,9 +74,10 @@ def combined_dataframes_with_rel_mean(
|
||||
df_comb = combine_dataframes_by_column(data, column)
|
||||
# Trim dataframes to the given timeframe
|
||||
df_comb = df_comb.iloc[(df_comb.index >= fromdt) & (df_comb.index < todt)]
|
||||
rel_mean = df_comb.pct_change().mean(axis=1).fillna(0).cumsum()
|
||||
df_comb["count"] = df_comb.count(axis=1)
|
||||
df_comb["mean"] = df_comb.mean(axis=1)
|
||||
df_comb["rel_mean"] = df_comb["mean"].pct_change().fillna(0).cumsum()
|
||||
df_comb["rel_mean"] = rel_mean
|
||||
return df_comb[["mean", "rel_mean", "count"]]
|
||||
|
||||
|
||||
@@ -333,7 +334,10 @@ def calculate_expectancy(trades: pd.DataFrame) -> tuple[float, float]:
|
||||
|
||||
|
||||
def calculate_sortino(
|
||||
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
|
||||
trades: pd.DataFrame,
|
||||
min_date: datetime | None,
|
||||
max_date: datetime | None,
|
||||
starting_balance: float,
|
||||
) -> float:
|
||||
"""
|
||||
Calculate sortino
|
||||
@@ -361,7 +365,10 @@ def calculate_sortino(
|
||||
|
||||
|
||||
def calculate_sharpe(
|
||||
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
|
||||
trades: pd.DataFrame,
|
||||
min_date: datetime | None,
|
||||
max_date: datetime | None,
|
||||
starting_balance: float,
|
||||
) -> float:
|
||||
"""
|
||||
Calculate sharpe
|
||||
@@ -388,7 +395,10 @@ def calculate_sharpe(
|
||||
|
||||
|
||||
def calculate_calmar(
|
||||
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
|
||||
trades: pd.DataFrame,
|
||||
min_date: datetime | None,
|
||||
max_date: datetime | None,
|
||||
starting_balance: float,
|
||||
) -> float:
|
||||
"""
|
||||
Calculate calmar
|
||||
|
||||
@@ -4,7 +4,7 @@ from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.exchange.exchange import Exchange
|
||||
|
||||
# isort: on
|
||||
from freqtrade.exchange.binance import Binance
|
||||
from freqtrade.exchange.binance import Binance, Binanceus, Binanceusdm
|
||||
from freqtrade.exchange.bingx import Bingx
|
||||
from freqtrade.exchange.bitget import Bitget
|
||||
from freqtrade.exchange.bitmart import Bitmart
|
||||
|
||||
@@ -17,7 +17,7 @@ from freqtrade.exchange.binance_public_data import (
|
||||
download_archive_trades,
|
||||
)
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas, Tickers
|
||||
from freqtrade.exchange.exchange_types import FtHas, Tickers
|
||||
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.util import FtTTLCache
|
||||
@@ -51,6 +51,8 @@ class Binance(Exchange):
|
||||
"funding_fee_candle_limit": 1000,
|
||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
||||
"stoploss_query_requires_stop_flag": True,
|
||||
"stoploss_algo_order_info_id": "actualOrderId",
|
||||
"tickers_have_price": False,
|
||||
"floor_leverage": True,
|
||||
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
|
||||
@@ -66,6 +68,7 @@ class Binance(Exchange):
|
||||
"BFUSD": "USDT",
|
||||
},
|
||||
}
|
||||
_can_use_data_download_fast = True
|
||||
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
(TradingMode.SPOT, MarginMode.NONE),
|
||||
@@ -145,20 +148,6 @@ class Binance(Exchange):
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def fetch_stoploss_order(
|
||||
self, order_id: str, pair: str, params: dict | None = None
|
||||
) -> CcxtOrder:
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
params = params or {}
|
||||
params.update({"stop": True})
|
||||
return self.fetch_order(order_id, pair, params)
|
||||
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
params = params or {}
|
||||
params.update({"stop": True})
|
||||
return self.cancel_order(order_id=order_id, pair=pair, params=params)
|
||||
|
||||
def get_historic_ohlcv(
|
||||
self,
|
||||
pair: str,
|
||||
@@ -193,7 +182,8 @@ class Binance(Exchange):
|
||||
return DataFrame(columns=DEFAULT_DATAFRAME_COLUMNS)
|
||||
|
||||
if (
|
||||
self._config["exchange"].get("only_from_ccxt", False)
|
||||
not self._can_use_data_download_fast
|
||||
or self._config["exchange"].get("only_from_ccxt", False)
|
||||
or
|
||||
# only download timeframes with significant improvements,
|
||||
# otherwise fall back to rest API
|
||||
@@ -417,7 +407,10 @@ class Binance(Exchange):
|
||||
) -> tuple[str, list[list]]:
|
||||
logger.info(f"Fetching trades for {pair} from Binance, {from_id=}, {since=}, {until=}")
|
||||
|
||||
if not self._config["exchange"].get("only_from_ccxt", False):
|
||||
if (
|
||||
not self._config["exchange"].get("only_from_ccxt", False)
|
||||
and self._can_use_data_download_fast
|
||||
):
|
||||
if from_id is None or not since:
|
||||
trades = await self._api_async.fetch_trades(
|
||||
pair,
|
||||
@@ -558,3 +551,28 @@ class Binance(Exchange):
|
||||
cache[ft_symbol] = delist_dt
|
||||
|
||||
return cache.get(pair, None)
|
||||
|
||||
|
||||
class Binanceusdm(Binance):
|
||||
"""Binacne USDM Exchange
|
||||
Same as Binance - only futures trading is supported (via ccxt).
|
||||
|
||||
Not actually necessary, binance should be preferred.
|
||||
"""
|
||||
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
(TradingMode.FUTURES, MarginMode.CROSS),
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||
]
|
||||
|
||||
|
||||
class Binanceus(Binance):
|
||||
"""Binance US exchange class.
|
||||
Minimal adjustment to disable futures trading for the US subsidiary of Binance
|
||||
"""
|
||||
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
(TradingMode.SPOT, MarginMode.NONE),
|
||||
]
|
||||
# binance vision does not have data for binanceus
|
||||
_can_use_data_download_fast = False
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,11 +31,11 @@ class Bitget(Exchange):
|
||||
"stop_price_prop": "stopPrice",
|
||||
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||
"stoploss_query_requires_stop_flag": True,
|
||||
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||
}
|
||||
_ft_has_futures: FtHas = {
|
||||
"mark_ohlcv_timeframe": "4h",
|
||||
"funding_fee_candle_limit": 100,
|
||||
"has_delisting": True,
|
||||
}
|
||||
@@ -129,9 +129,6 @@ class Bitget(Exchange):
|
||||
|
||||
return self._fetch_stop_order_fallback(order_id, pair)
|
||||
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
||||
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
|
||||
|
||||
@retrier
|
||||
def additional_exchange_init(self) -> None:
|
||||
"""
|
||||
|
||||
@@ -38,8 +38,6 @@ class Bybit(Exchange):
|
||||
}
|
||||
_ft_has_futures: FtHas = {
|
||||
"ohlcv_has_history": True,
|
||||
"mark_ohlcv_timeframe": "4h",
|
||||
"funding_fee_timeframe": "8h",
|
||||
"funding_fee_candle_limit": 200,
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||
|
||||
@@ -47,14 +47,16 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool:
|
||||
f"{', '.join(available_exchanges())}"
|
||||
)
|
||||
|
||||
valid, reason, _ = validate_exchange(exchange)
|
||||
valid, reason, _, _ = validate_exchange(exchange)
|
||||
if not valid:
|
||||
if check_for_bad:
|
||||
raise OperationalException(
|
||||
f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}'
|
||||
f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}.'
|
||||
)
|
||||
else:
|
||||
logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}')
|
||||
logger.warning(
|
||||
f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}.'
|
||||
)
|
||||
|
||||
if MAP_EXCHANGE_CHILDCLASS.get(exchange, exchange) in SUPPORTED_EXCHANGES:
|
||||
logger.info(
|
||||
|
||||
@@ -36,17 +36,16 @@ API_RETRY_COUNT = 4
|
||||
API_FETCH_ORDER_RETRY_COUNT = 5
|
||||
|
||||
BAD_EXCHANGES = {
|
||||
"bitmex": "Various reasons.",
|
||||
"probit": "Requires additional, regular calls to `signIn()`.",
|
||||
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
|
||||
"kucoinfutures": "Unsupported futures exchange.",
|
||||
"poloniexfutures": "Unsupported futures exchange.",
|
||||
"binancecoinm": "Unsupported futures exchange.",
|
||||
"bitmex": "Various reasons",
|
||||
"probit": "Requires additional, regular calls to `signIn()`",
|
||||
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders",
|
||||
"krakenfutures": "Unsupported futures exchange",
|
||||
"kucoinfutures": "Unsupported futures exchange",
|
||||
"poloniexfutures": "Unsupported futures exchange",
|
||||
"binancecoinm": "Unsupported futures exchange",
|
||||
}
|
||||
|
||||
MAP_EXCHANGE_CHILDCLASS = {
|
||||
"binanceus": "binance",
|
||||
"binanceusdm": "binance",
|
||||
"okex": "okx",
|
||||
"gateio": "gate",
|
||||
"huboi": "htx",
|
||||
@@ -54,6 +53,8 @@ MAP_EXCHANGE_CHILDCLASS = {
|
||||
|
||||
SUPPORTED_EXCHANGES = [
|
||||
"binance",
|
||||
"binanceus",
|
||||
"binanceusdm",
|
||||
"bingx",
|
||||
"bitmart",
|
||||
"bitget",
|
||||
@@ -78,28 +79,35 @@ EXCHANGE_HAS_REQUIRED: dict[str, list[str]] = {
|
||||
"fetchOHLCV": [],
|
||||
}
|
||||
|
||||
EXCHANGE_HAS_OPTIONAL = [
|
||||
EXCHANGE_HAS_OPTIONAL: dict[str, list[str]] = {
|
||||
# Private
|
||||
"fetchMyTrades", # Trades for order - fee detection
|
||||
"createLimitOrder",
|
||||
"createMarketOrder", # Either OR for orders
|
||||
# 'setLeverage', # Margin/Futures trading
|
||||
# 'setMarginMode', # Margin/Futures trading
|
||||
# 'fetchFundingHistory', # Futures trading
|
||||
"fetchMyTrades": [], # Trades for order - fee detection
|
||||
"createLimitOrder": [],
|
||||
"createMarketOrder": [], # Either OR for orders
|
||||
# Public
|
||||
"fetchOrderBook",
|
||||
"fetchL2OrderBook",
|
||||
"fetchTicker", # OR for pricing
|
||||
"fetchTickers", # For volumepairlist?
|
||||
"fetchTrades", # Downloading trades data
|
||||
# 'fetchFundingRateHistory', # Futures trading
|
||||
# 'fetchPositions', # Futures trading
|
||||
# 'fetchLeverageTiers', # Futures initialization
|
||||
# 'fetchMarketLeverageTiers', # Futures initialization
|
||||
# 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance...
|
||||
"fetchOrderBook": [],
|
||||
"fetchL2OrderBook": [],
|
||||
"fetchTicker": [], # OR for pricing
|
||||
"fetchTickers": [], # For volumepairlist?
|
||||
"fetchTrades": [], # Downloading trades data
|
||||
"fetchOrders": ["fetchOpenOrders", "fetchClosedOrders"], # , # Refinding balance...
|
||||
# ccxt.pro
|
||||
"watchOHLCV",
|
||||
]
|
||||
"watchOHLCV": [],
|
||||
}
|
||||
|
||||
EXCHANGE_HAS_OPTIONAL_FUTURES: dict[str, list[str]] = {
|
||||
# private
|
||||
"setLeverage": [], # Margin/Futures trading
|
||||
"setMarginMode": [], # Margin/Futures trading
|
||||
"fetchFundingHistory": [], # Futures trading
|
||||
# Public
|
||||
"fetchFundingRateHistory": [], # Futures trading
|
||||
"fetchPositions": [], # Futures trading
|
||||
"fetchLeverageTiers": ["fetchMarketLeverageTiers"], # Futures initialization
|
||||
"fetchMarkOHLCV": [],
|
||||
"fetchIndexOHLCV": [], # Futures additional data
|
||||
"fetchPremiumIndexOHLCV": [],
|
||||
}
|
||||
|
||||
|
||||
def calculate_backoff(retrycount, max_retries):
|
||||
|
||||
+212
-71
@@ -73,6 +73,7 @@ from freqtrade.exchange.exchange_types import (
|
||||
CcxtPosition,
|
||||
FtHas,
|
||||
FundingRate,
|
||||
LeverageTier,
|
||||
OHLCVResponse,
|
||||
OrderBook,
|
||||
Ticker,
|
||||
@@ -104,7 +105,7 @@ from freqtrade.misc import (
|
||||
deep_merge_dicts,
|
||||
file_dump_json,
|
||||
file_load_json,
|
||||
safe_value_fallback2,
|
||||
safe_value_fallback,
|
||||
)
|
||||
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
|
||||
@@ -131,6 +132,7 @@ class Exchange:
|
||||
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
|
||||
"stoploss_order_types": {},
|
||||
"stoploss_blocks_assets": True, # By default stoploss orders block assets
|
||||
"stoploss_query_requires_stop_flag": False, # Require "stop": True" to fetch stop orders
|
||||
"order_time_in_force": ["GTC"],
|
||||
"ohlcv_params": {},
|
||||
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
||||
@@ -152,8 +154,8 @@ class Exchange:
|
||||
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
||||
"l2_limit_upper": None, # Upper limit for L2 limit
|
||||
"mark_ohlcv_price": "mark",
|
||||
"mark_ohlcv_timeframe": "8h",
|
||||
"funding_fee_timeframe": "8h",
|
||||
"mark_ohlcv_timeframe": "1h",
|
||||
"funding_fee_timeframe": "1h",
|
||||
"ccxt_futures_name": "swap",
|
||||
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
||||
"order_props_in_contracts": ["amount", "filled", "remaining"],
|
||||
@@ -193,29 +195,30 @@ class Exchange:
|
||||
self._exchange_ws: ExchangeWS | None = None
|
||||
self._markets: dict = {}
|
||||
self._trading_fees: dict[str, Any] = {}
|
||||
self._leverage_tiers: dict[str, list[dict]] = {}
|
||||
self._leverage_tiers: dict[str, list[LeverageTier]] = {}
|
||||
# Lock event loop. This is necessary to avoid race-conditions when using force* commands
|
||||
# Due to funding fee fetching.
|
||||
self._loop_lock = Lock()
|
||||
self.loop = self._init_async_loop()
|
||||
self._config: Config = {}
|
||||
self._config: Config = config
|
||||
|
||||
# Leverage properties
|
||||
self.trading_mode: TradingMode = TradingMode(
|
||||
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(
|
||||
MarginMode(config.get("margin_mode"))
|
||||
if config.get("margin_mode")
|
||||
MarginMode(self._config.get("margin_mode"))
|
||||
if self._config.get("margin_mode")
|
||||
else self._supported_trading_mode_margin_pairs[0][1]
|
||||
)
|
||||
config["trading_mode"] = self.trading_mode
|
||||
config["margin_mode"] = self.margin_mode
|
||||
config["candle_type_def"] = CandleType.get_default(self.trading_mode)
|
||||
self._config.update(config)
|
||||
self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
|
||||
self._config["trading_mode"] = self.trading_mode
|
||||
self._config["margin_mode"] = self.margin_mode
|
||||
self._config["candle_type_def"] = CandleType.get_default(self.trading_mode)
|
||||
self.liquidation_buffer = self._config.get("liquidation_buffer", 0.05)
|
||||
|
||||
exchange_conf: ExchangeConfig = exchange_config if exchange_config else config["exchange"]
|
||||
exchange_conf: ExchangeConfig = (
|
||||
exchange_config if exchange_config else self._config["exchange"]
|
||||
)
|
||||
|
||||
# Deep merge ft_has with default ft_has options
|
||||
# Must be called before ft_has is used.
|
||||
@@ -246,14 +249,14 @@ class Exchange:
|
||||
# Holds all open sell orders for dry_run
|
||||
self._dry_run_open_orders: dict[str, Any] = {}
|
||||
|
||||
if config["dry_run"]:
|
||||
if self._config["dry_run"]:
|
||||
logger.info("Instance is running with dry_run enabled")
|
||||
logger.info(f"Using CCXT {ccxt.__version__}")
|
||||
|
||||
# Don't remove exchange credentials for dry-run or if always_require_api_keys is set
|
||||
remove_exchange_credentials(
|
||||
exchange_conf,
|
||||
not self._ft_has["always_require_api_keys"] and config.get("dry_run", False),
|
||||
not self._ft_has["always_require_api_keys"] and self._config.get("dry_run", False),
|
||||
)
|
||||
self.log_responses = exchange_conf.get("log_responses", False)
|
||||
|
||||
@@ -294,7 +297,7 @@ class Exchange:
|
||||
if validate:
|
||||
# Initial markets load
|
||||
self.reload_markets(True, load_leverage_tiers=False)
|
||||
self.validate_config(config)
|
||||
self.validate_config(self._config)
|
||||
|
||||
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
||||
self.fill_leverage_tiers()
|
||||
@@ -478,7 +481,7 @@ class Exchange:
|
||||
def _log_exchange_response(self, endpoint: str, response, *, add_info=None) -> None:
|
||||
"""Log exchange responses"""
|
||||
if self.log_responses:
|
||||
add_info_str = "" if add_info is None else f" {add_info}: "
|
||||
add_info_str = "" if add_info is None else f"{add_info}: "
|
||||
logger.info(f"API {endpoint}: {add_info_str}{response}")
|
||||
|
||||
def ohlcv_candle_limit(
|
||||
@@ -707,7 +710,7 @@ class Exchange:
|
||||
self._markets = self._api_async.markets
|
||||
self._api.set_markets_from_exchange(self._api_async)
|
||||
# Assign options array, as it contains some temporary information from the exchange.
|
||||
# TODO: investigate with ccxt if it's safe to remove `.options`
|
||||
# ccxt does not implicitly copy options over in set_markets_from_exchange
|
||||
self._api.options = self._api_async.options
|
||||
if self._exchange_ws:
|
||||
# Set markets to avoid reloading on websocket api
|
||||
@@ -877,19 +880,20 @@ class Exchange:
|
||||
# Only allow 5 calls per pair to somewhat limit the impact
|
||||
raise ConfigurationError(
|
||||
f"This strategy requires {startup_candles} candles to start, "
|
||||
"which is more than 5x "
|
||||
f"which is more than 5x ({candle_limit * 5 - 1} candles) "
|
||||
f"the amount of candles {self.name} provides for {timeframe}."
|
||||
)
|
||||
elif required_candle_call_count > 1:
|
||||
raise ConfigurationError(
|
||||
f"This strategy requires {startup_candles} candles to start, which is more than "
|
||||
f"This strategy requires {startup_candles} candles to start, "
|
||||
f"which is more than ({candle_limit - 1} candles) "
|
||||
f"the amount of candles {self.name} provides for {timeframe}."
|
||||
)
|
||||
if required_candle_call_count > 1:
|
||||
logger.warning(
|
||||
f"Using {required_candle_call_count} calls to get OHLCV. "
|
||||
f"This can result in slower operations for the bot. Please check "
|
||||
f"if you really need {startup_candles} candles for your strategy"
|
||||
f"if you really need {startup_candles} candles for your strategy."
|
||||
)
|
||||
return required_candle_call_count
|
||||
|
||||
@@ -1119,6 +1123,7 @@ class Exchange:
|
||||
leverage: float,
|
||||
params: dict | None = None,
|
||||
stop_loss: bool = False,
|
||||
stop_price: float | None = None,
|
||||
) -> CcxtOrder:
|
||||
now = dt_now()
|
||||
order_id = f"dry_run_{side}_{pair}_{now.timestamp()}"
|
||||
@@ -1145,7 +1150,7 @@ class Exchange:
|
||||
}
|
||||
if stop_loss:
|
||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||
dry_order[self._ft_has["stop_price_prop"]] = dry_order["price"]
|
||||
dry_order[self._ft_has["stop_price_prop"]] = stop_price or dry_order["price"]
|
||||
# Workaround to avoid filling stoploss orders immediately
|
||||
dry_order["ft_order_type"] = "stoploss"
|
||||
orderbook: OrderBook | None = None
|
||||
@@ -1163,7 +1168,11 @@ class Exchange:
|
||||
|
||||
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
||||
# Update market order pricing
|
||||
average = self.get_dry_market_fill_price(pair, side, amount, rate, orderbook)
|
||||
slippage = 0.05
|
||||
worst_rate = rate * ((1 + slippage) if side == "buy" else (1 - slippage))
|
||||
average = self.get_dry_market_fill_price(
|
||||
pair, side, amount, rate, worst_rate, orderbook
|
||||
)
|
||||
dry_order.update(
|
||||
{
|
||||
"average": average,
|
||||
@@ -1203,7 +1212,13 @@ class Exchange:
|
||||
return dry_order
|
||||
|
||||
def get_dry_market_fill_price(
|
||||
self, pair: str, side: str, amount: float, rate: float, orderbook: OrderBook | None
|
||||
self,
|
||||
pair: str,
|
||||
side: str,
|
||||
amount: float,
|
||||
rate: float,
|
||||
worst_rate: float,
|
||||
orderbook: OrderBook | None,
|
||||
) -> float:
|
||||
"""
|
||||
Get the market order fill price based on orderbook interpolation
|
||||
@@ -1212,8 +1227,6 @@ class Exchange:
|
||||
if not orderbook:
|
||||
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||
ob_type: OBLiteral = "asks" if side == "buy" else "bids"
|
||||
slippage = 0.05
|
||||
max_slippage_val = rate * ((1 + slippage) if side == "buy" else (1 - slippage))
|
||||
|
||||
remaining_amount = amount
|
||||
filled_value = 0.0
|
||||
@@ -1237,11 +1250,10 @@ class Exchange:
|
||||
forecast_avg_filled_price = max(filled_value, 0) / amount
|
||||
# Limit max. slippage to specified value
|
||||
if side == "buy":
|
||||
forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
|
||||
forecast_avg_filled_price = min(forecast_avg_filled_price, worst_rate)
|
||||
|
||||
else:
|
||||
forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
|
||||
|
||||
forecast_avg_filled_price = max(forecast_avg_filled_price, worst_rate)
|
||||
return self.price_to_precision(pair, forecast_avg_filled_price)
|
||||
|
||||
return rate
|
||||
@@ -1253,13 +1265,15 @@ class Exchange:
|
||||
limit: float,
|
||||
orderbook: OrderBook | None = None,
|
||||
offset: float = 0.0,
|
||||
is_stop: bool = False,
|
||||
) -> bool:
|
||||
if not self.exchange_has("fetchL2OrderBook"):
|
||||
return True
|
||||
# True unless checking a stoploss order
|
||||
return not is_stop
|
||||
if not orderbook:
|
||||
orderbook = self.fetch_l2_order_book(pair, 1)
|
||||
try:
|
||||
if side == "buy":
|
||||
if (side == "buy" and not is_stop) or (side == "sell" and is_stop):
|
||||
price = orderbook["asks"][0][0]
|
||||
if limit * (1 - offset) >= price:
|
||||
return True
|
||||
@@ -1278,6 +1292,38 @@ class Exchange:
|
||||
"""
|
||||
Check dry-run limit order fill and update fee (if it filled).
|
||||
"""
|
||||
if order["status"] != "closed" and order.get("ft_order_type") == "stoploss":
|
||||
pair = order["symbol"]
|
||||
if not orderbook and self.exchange_has("fetchL2OrderBook"):
|
||||
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||
price = safe_value_fallback(order, self._ft_has["stop_price_prop"], "price")
|
||||
crossed = self._dry_is_price_crossed(
|
||||
pair, order["side"], price, orderbook, is_stop=True
|
||||
)
|
||||
if crossed:
|
||||
average = self.get_dry_market_fill_price(
|
||||
pair,
|
||||
order["side"],
|
||||
order["amount"],
|
||||
price,
|
||||
worst_rate=order["price"],
|
||||
orderbook=orderbook,
|
||||
)
|
||||
order.update(
|
||||
{
|
||||
"status": "closed",
|
||||
"filled": order["amount"],
|
||||
"remaining": 0,
|
||||
"average": average,
|
||||
"cost": order["amount"] * average,
|
||||
}
|
||||
)
|
||||
self.add_dry_order_fee(
|
||||
pair,
|
||||
order,
|
||||
"taker" if immediate else "maker",
|
||||
)
|
||||
return order
|
||||
if (
|
||||
order["status"] != "closed"
|
||||
and order["type"] in ["limit"]
|
||||
@@ -1362,8 +1408,9 @@ class Exchange:
|
||||
amount: float,
|
||||
rate: float,
|
||||
leverage: float,
|
||||
reduceOnly: bool = False,
|
||||
time_in_force: str = "GTC",
|
||||
reduceOnly: bool = False,
|
||||
initial_order: bool = True,
|
||||
) -> CcxtOrder:
|
||||
if self._config["dry_run"]:
|
||||
dry_order = self.create_dry_run_order(
|
||||
@@ -1380,7 +1427,7 @@ class Exchange:
|
||||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||
|
||||
if not reduceOnly:
|
||||
self._lev_prep(pair, leverage, side)
|
||||
self._lev_prep(pair, leverage, side, accept_fail=not initial_order)
|
||||
|
||||
order = self._api.create_order(
|
||||
pair,
|
||||
@@ -1517,8 +1564,9 @@ class Exchange:
|
||||
ordertype,
|
||||
side,
|
||||
amount,
|
||||
stop_price_norm,
|
||||
limit_rate or stop_price_norm,
|
||||
stop_loss=True,
|
||||
stop_price=stop_price_norm,
|
||||
leverage=leverage,
|
||||
)
|
||||
return dry_order
|
||||
@@ -1642,7 +1690,24 @@ class Exchange:
|
||||
def fetch_stoploss_order(
|
||||
self, order_id: str, pair: str, params: dict | None = None
|
||||
) -> CcxtOrder:
|
||||
return self.fetch_order(order_id, pair, params)
|
||||
if self.get_option("stoploss_query_requires_stop_flag"):
|
||||
params = params or {}
|
||||
params["stop"] = True
|
||||
order = self.fetch_order(order_id, pair, params)
|
||||
val = self.get_option("stoploss_algo_order_info_id")
|
||||
if val and order.get("status", "open") == "closed":
|
||||
if new_orderid := order.get("info", {}).get(val):
|
||||
# Fetch real order, which was placed by the algo order.
|
||||
actual_order = self.fetch_order(order_id=new_orderid, pair=pair, params=None)
|
||||
actual_order["id_stop"] = actual_order["id"]
|
||||
actual_order["id"] = order_id
|
||||
actual_order["type"] = "stoploss"
|
||||
actual_order["stopPrice"] = order.get("stopPrice")
|
||||
actual_order["status_stop"] = "triggered"
|
||||
|
||||
return actual_order
|
||||
|
||||
return order
|
||||
|
||||
def fetch_order_or_stoploss_order(
|
||||
self, order_id: str, pair: str, stoploss_order: bool = False
|
||||
@@ -1696,6 +1761,9 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
||||
if self.get_option("stoploss_query_requires_stop_flag"):
|
||||
params = params or {}
|
||||
params["stop"] = True
|
||||
return self.cancel_order(order_id, pair, params)
|
||||
|
||||
def is_cancel_order_result_suitable(self, corder) -> TypeGuard[CcxtOrder]:
|
||||
@@ -1760,16 +1828,16 @@ class Exchange:
|
||||
return order
|
||||
|
||||
@retrier
|
||||
def get_balances(self) -> CcxtBalances:
|
||||
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||
try:
|
||||
balances = self._api.fetch_balance()
|
||||
balances = self._api.fetch_balance(params or {})
|
||||
# Remove additional info from ccxt results
|
||||
balances.pop("info", None)
|
||||
balances.pop("free", None)
|
||||
balances.pop("total", None)
|
||||
balances.pop("used", None)
|
||||
|
||||
self._log_exchange_response("fetch_balance", balances)
|
||||
self._log_exchange_response("fetch_balance", balances, add_info=params)
|
||||
return balances
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
@@ -1781,7 +1849,9 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def fetch_positions(self, pair: str | None = None) -> list[CcxtPosition]:
|
||||
def fetch_positions(
|
||||
self, pair: str | None = None, params: dict | None = None
|
||||
) -> list[CcxtPosition]:
|
||||
"""
|
||||
Fetch positions from the exchange.
|
||||
If no pair is given, all positions are returned.
|
||||
@@ -1790,10 +1860,10 @@ class Exchange:
|
||||
if self._config["dry_run"] or self.trading_mode != TradingMode.FUTURES:
|
||||
return []
|
||||
try:
|
||||
symbols = []
|
||||
symbols = None
|
||||
if pair:
|
||||
symbols.append(pair)
|
||||
positions: list[CcxtPosition] = self._api.fetch_positions(symbols)
|
||||
symbols = [pair]
|
||||
positions: list[CcxtPosition] = self._api.fetch_positions(symbols, params=params or {})
|
||||
self._log_exchange_response("fetch_positions", positions)
|
||||
return positions
|
||||
except ccxt.DDoSProtection as e:
|
||||
@@ -1993,12 +2063,13 @@ class Exchange:
|
||||
"""
|
||||
return self._config["stake_currency"]
|
||||
|
||||
def get_conversion_rate(self, coin: str, currency: str) -> float | None:
|
||||
def get_conversion_rate(self, coin: str, currency: str, *, cached=True) -> float | None:
|
||||
"""
|
||||
Quick and cached way to get conversion rate one currency to the other.
|
||||
Can then be used as "rate * amount" to convert between currencies.
|
||||
:param coin: Coin to convert
|
||||
:param currency: Currency to convert to
|
||||
:param cached: Allow cached tickers, default True
|
||||
:returns: Conversion rate from coin to currency
|
||||
:raises: ExchangeErrors
|
||||
"""
|
||||
@@ -2009,13 +2080,13 @@ class Exchange:
|
||||
currency = proxy_currency
|
||||
if coin == currency:
|
||||
return 1.0
|
||||
tickers = self.get_tickers(cached=True)
|
||||
tickers = self.get_tickers(cached=cached)
|
||||
try:
|
||||
for pair in self.get_valid_pair_combination(coin, currency):
|
||||
ticker: Ticker | None = tickers.get(pair, None)
|
||||
if not ticker:
|
||||
tickers_other: Tickers = self.get_tickers(
|
||||
cached=True,
|
||||
cached=cached,
|
||||
market_type=(
|
||||
TradingMode.SPOT
|
||||
if self.trading_mode != TradingMode.SPOT
|
||||
@@ -2024,7 +2095,7 @@ class Exchange:
|
||||
)
|
||||
ticker = tickers_other.get(pair, None)
|
||||
if ticker:
|
||||
rate: float | None = safe_value_fallback2(ticker, ticker, "last", "ask", None)
|
||||
rate: float | None = safe_value_fallback(ticker, "last", "ask", None)
|
||||
if rate and pair.startswith(currency) and not pair.endswith(currency):
|
||||
rate = 1.0 / rate
|
||||
return rate
|
||||
@@ -2324,6 +2395,16 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
||||
"""
|
||||
Return order id or id_stop (for conditional orders) based on exchange settings
|
||||
|
||||
:param order: ccxt order dict
|
||||
:return: correct order id
|
||||
"""
|
||||
if self.get_option("stoploss_query_requires_stop_flag") and (
|
||||
order["type"] in ("stoploss", "stop")
|
||||
):
|
||||
return safe_value_fallback(order, "id_stop", "id")
|
||||
return order["id"]
|
||||
|
||||
@retrier
|
||||
@@ -2483,7 +2564,13 @@ class Exchange:
|
||||
)
|
||||
)
|
||||
logger.debug(f"Downloaded data for {pair} from ccxt with length {len(data)}.")
|
||||
return ohlcv_to_dataframe(data, timeframe, pair, fill_missing=False, drop_incomplete=True)
|
||||
# funding_rates are always complete, so never need to be dropped.
|
||||
drop_incomplete = (
|
||||
self._ohlcv_partial_candle if candle_type != CandleType.FUNDING_RATE else False
|
||||
)
|
||||
return ohlcv_to_dataframe(
|
||||
data, timeframe, pair, fill_missing=False, drop_incomplete=drop_incomplete
|
||||
)
|
||||
|
||||
async def _async_get_historic_ohlcv(
|
||||
self,
|
||||
@@ -2646,24 +2733,25 @@ class Exchange:
|
||||
input_coroutines: list[Coroutine[Any, Any, OHLCVResponse]] = []
|
||||
cached_pairs = []
|
||||
for pair, timeframe, candle_type in set(pair_list):
|
||||
invalid_funding = (
|
||||
candle_type == CandleType.FUNDING_RATE
|
||||
and timeframe != self.get_option("funding_fee_timeframe")
|
||||
)
|
||||
if candle_type == CandleType.FUNDING_RATE and timeframe != (
|
||||
ff_tf := self.get_option("funding_fee_timeframe")
|
||||
):
|
||||
# TODO: does this message make sense? would docs be better?
|
||||
# if any, this should be cached to avoid log spam!
|
||||
logger.warning(
|
||||
f"Wrong funding rate timeframe {timeframe} for pair {pair}, "
|
||||
f"downloading {ff_tf} instead."
|
||||
)
|
||||
timeframe = ff_tf
|
||||
invalid_timeframe = timeframe not in self.timeframes and candle_type in (
|
||||
CandleType.SPOT,
|
||||
CandleType.FUTURES,
|
||||
)
|
||||
if invalid_timeframe or invalid_funding:
|
||||
timeframes_ = (
|
||||
", ".join(self.timeframes)
|
||||
if candle_type != CandleType.FUNDING_RATE
|
||||
else self.get_option("funding_fee_timeframe")
|
||||
)
|
||||
if invalid_timeframe:
|
||||
logger.warning(
|
||||
f"Cannot download ({pair}, {timeframe}, {candle_type}) combination as this "
|
||||
f"timeframe is not available on {self.name}. Available timeframes are "
|
||||
f"{timeframes_}."
|
||||
f"{', '.join(self.timeframes)}."
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -2700,7 +2788,11 @@ class Exchange:
|
||||
has_cache = cache and (pair, timeframe, c_type) in self._klines
|
||||
# in case of existing cache, fill_missing happens after concatenation
|
||||
ohlcv_df = ohlcv_to_dataframe(
|
||||
ticks, timeframe, pair=pair, fill_missing=not has_cache, drop_incomplete=drop_incomplete
|
||||
ticks,
|
||||
timeframe,
|
||||
pair=pair,
|
||||
fill_missing=not has_cache and c_type != CandleType.FUNDING_RATE,
|
||||
drop_incomplete=drop_incomplete,
|
||||
)
|
||||
# keeping parsed dataframe in cache
|
||||
if cache:
|
||||
@@ -2711,7 +2803,7 @@ class Exchange:
|
||||
concat([old, ohlcv_df], axis=0),
|
||||
timeframe,
|
||||
pair,
|
||||
fill_missing=True,
|
||||
fill_missing=c_type != CandleType.FUNDING_RATE,
|
||||
drop_incomplete=False,
|
||||
)
|
||||
candle_limit = self.ohlcv_candle_limit(timeframe, self._config["candle_type_def"])
|
||||
@@ -2804,8 +2896,11 @@ class Exchange:
|
||||
}
|
||||
pairs_to_download = [p for p in pairs if p not in candles]
|
||||
if pairs_to_download:
|
||||
candles = self.refresh_latest_ohlcv(pairs_to_download, since_ms=since_ms, cache=False)
|
||||
for c, val in candles.items():
|
||||
candles_new = self.refresh_latest_ohlcv(
|
||||
pairs_to_download, since_ms=since_ms, cache=False
|
||||
)
|
||||
for c, val in candles_new.items():
|
||||
candles[c] = val
|
||||
self._expiring_candle_cache[(c[1], since_ms)][c] = val
|
||||
return candles
|
||||
|
||||
@@ -2846,9 +2941,10 @@ class Exchange:
|
||||
timeframe, candle_type=candle_type, since_ms=since_ms
|
||||
)
|
||||
|
||||
if candle_type and candle_type not in (CandleType.SPOT, CandleType.FUTURES):
|
||||
params.update({"price": candle_type.value})
|
||||
if candle_type != CandleType.FUNDING_RATE:
|
||||
if candle_type and candle_type not in (CandleType.SPOT, CandleType.FUTURES):
|
||||
self.verify_candle_type_support(candle_type)
|
||||
params.update({"price": str(candle_type)})
|
||||
data = await self._api_async.fetch_ohlcv(
|
||||
pair, timeframe=timeframe, since=since_ms, limit=candle_limit, params=params
|
||||
)
|
||||
@@ -2913,6 +3009,38 @@ class Exchange:
|
||||
data = [[x["timestamp"], x["fundingRate"], 0, 0, 0, 0] for x in data]
|
||||
return data
|
||||
|
||||
def check_candle_type_support(self, candle_type: CandleType) -> bool:
|
||||
"""
|
||||
Check that the exchange supports the given candle type.
|
||||
:param candle_type: CandleType to verify
|
||||
:return: True if supported, False otherwise
|
||||
"""
|
||||
if candle_type == CandleType.FUNDING_RATE:
|
||||
if not self.exchange_has("fetchFundingRateHistory"):
|
||||
return False
|
||||
elif candle_type not in (CandleType.SPOT, CandleType.FUTURES):
|
||||
mapping = {
|
||||
CandleType.MARK: "fetchMarkOHLCV",
|
||||
CandleType.INDEX: "fetchIndexOHLCV",
|
||||
CandleType.PREMIUMINDEX: "fetchPremiumIndexOHLCV",
|
||||
CandleType.FUNDING_RATE: "fetchFundingRateHistory",
|
||||
}
|
||||
_method = mapping.get(candle_type, "fetchOHLCV")
|
||||
if not self.exchange_has(_method):
|
||||
return False
|
||||
return True
|
||||
|
||||
def verify_candle_type_support(self, candle_type: CandleType) -> None:
|
||||
"""
|
||||
Verify that the exchange supports the given candle type.
|
||||
:param candle_type: CandleType to verify
|
||||
:raises OperationalException: if the candle type is not supported
|
||||
"""
|
||||
if not self.check_candle_type_support(candle_type):
|
||||
raise OperationalException(
|
||||
f"Exchange {self._api.name} does not support fetching {candle_type} candles."
|
||||
)
|
||||
|
||||
# fetch Trade data stuff
|
||||
|
||||
def needed_candle_for_trades_ms(self, timeframe: str, candle_type: CandleType) -> int:
|
||||
@@ -3497,7 +3625,7 @@ class Exchange:
|
||||
pair_tiers.append(self.parse_leverage_tier(tier))
|
||||
self._leverage_tiers[pair] = pair_tiers
|
||||
|
||||
def parse_leverage_tier(self, tier) -> dict:
|
||||
def parse_leverage_tier(self, tier) -> LeverageTier:
|
||||
info = tier.get("info", {})
|
||||
return {
|
||||
"minNotional": tier["minNotional"],
|
||||
@@ -3538,7 +3666,11 @@ class Exchange:
|
||||
for tier in pair_tiers:
|
||||
# Adjust notional by leverage to do a proper comparison
|
||||
min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"])
|
||||
max_stake = tier["maxNotional"] / tier["maxLeverage"]
|
||||
max_stake = (
|
||||
tier["maxNotional"] / tier["maxLeverage"]
|
||||
if tier["maxNotional"] is not None
|
||||
else float("inf")
|
||||
)
|
||||
prior_max_lev = tier["maxLeverage"]
|
||||
if min_stake <= stake_amount <= max_stake:
|
||||
return tier["maxLeverage"]
|
||||
@@ -3740,10 +3872,11 @@ class Exchange:
|
||||
:param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
|
||||
:param futures_funding_rate: Fake funding rate to use if funding_rates are not available
|
||||
"""
|
||||
relevant_cols = ["date", "open_mark", "open_fund"]
|
||||
if futures_funding_rate is None:
|
||||
return mark_rates.merge(
|
||||
funding_rates, on="date", how="inner", suffixes=["_mark", "_fund"]
|
||||
)
|
||||
)[relevant_cols]
|
||||
else:
|
||||
if len(funding_rates) == 0:
|
||||
# No funding rate candles - full fillup with fallback variable
|
||||
@@ -3756,15 +3889,23 @@ class Exchange:
|
||||
"low": "low_mark",
|
||||
"volume": "volume_mark",
|
||||
}
|
||||
)
|
||||
)[relevant_cols]
|
||||
|
||||
else:
|
||||
# Fill up missing funding_rate candles with fallback value
|
||||
combined = mark_rates.merge(
|
||||
funding_rates, on="date", how="left", suffixes=["_mark", "_fund"]
|
||||
)
|
||||
combined["open_fund"] = combined["open_fund"].fillna(futures_funding_rate)
|
||||
return combined
|
||||
# Fill only leading missing funding rates so gaps stay untouched
|
||||
first_valid_idx = combined["open_fund"].first_valid_index()
|
||||
if first_valid_idx is None:
|
||||
combined["open_fund"] = futures_funding_rate
|
||||
else:
|
||||
is_leading_na = (combined.index <= first_valid_idx) & combined[
|
||||
"open_fund"
|
||||
].isna()
|
||||
combined.loc[is_leading_na, "open_fund"] = futures_funding_rate
|
||||
return combined[relevant_cols].dropna()
|
||||
|
||||
def calculate_funding_fees(
|
||||
self,
|
||||
|
||||
@@ -19,6 +19,8 @@ class FtHas(TypedDict, total=False):
|
||||
stop_price_type_value_mapping: dict
|
||||
stoploss_order_types: dict[str, str]
|
||||
stoploss_blocks_assets: bool
|
||||
stoploss_query_requires_stop_flag: bool
|
||||
stoploss_algo_order_info_id: str
|
||||
# ohlcv
|
||||
ohlcv_params: dict
|
||||
ohlcv_candle_limit: int
|
||||
@@ -113,5 +115,27 @@ class CcxtPosition(TypedDict):
|
||||
|
||||
CcxtOrder = dict[str, Any]
|
||||
|
||||
|
||||
class LeverageTier(TypedDict):
|
||||
"""
|
||||
Represents a single leverage tier returned by the exchange.
|
||||
|
||||
Attributes:
|
||||
minNotional: Minimum notional value (quote currency) for which this tier applies.
|
||||
maxNotional: Maximum notional value (quote currency) for which this tier applies.
|
||||
When ``maxNotional`` is ``None``, the tier is unbounded on the upper side,
|
||||
i.e. there is no maximum notional limit for this tier
|
||||
maintenanceMarginRate: Maintenance margin rate for this tier (fraction, e.g. 0.005 for 0.5%)
|
||||
maxLeverage: Maximum leverage allowed for this tier
|
||||
maintAmt: Optional fixed maintenance margin amount, if provided by the exchange
|
||||
"""
|
||||
|
||||
minNotional: float
|
||||
maxNotional: float | None
|
||||
maintenanceMarginRate: float
|
||||
maxLeverage: float
|
||||
maintAmt: float | None
|
||||
|
||||
|
||||
# pair, timeframe, candleType, OHLCV, drop last?,
|
||||
OHLCVResponse = tuple[str, str, CandleType, list, bool]
|
||||
|
||||
@@ -22,6 +22,7 @@ from ccxt import (
|
||||
from freqtrade.exchange.common import (
|
||||
BAD_EXCHANGES,
|
||||
EXCHANGE_HAS_OPTIONAL,
|
||||
EXCHANGE_HAS_OPTIONAL_FUTURES,
|
||||
EXCHANGE_HAS_REQUIRED,
|
||||
MAP_EXCHANGE_CHILDCLASS,
|
||||
SUPPORTED_EXCHANGES,
|
||||
@@ -53,7 +54,22 @@ def available_exchanges(ccxt_module: CcxtModuleType | None = None) -> list[str]:
|
||||
return [x for x in exchanges if validate_exchange(x)[0]]
|
||||
|
||||
|
||||
def validate_exchange(exchange: str) -> tuple[bool, str, ccxt.Exchange | None]:
|
||||
def _exchange_has_helper(ex_mod: ccxt.Exchange, required: dict[str, list[str]]) -> list[str]:
|
||||
"""
|
||||
Checks availability of methods (or their replacement)s in ex_mod.has
|
||||
:param ex_mod: ccxt Exchange module
|
||||
:param required: dict of required methods, with possible replacement methods as list
|
||||
:return: list of missing required methods
|
||||
"""
|
||||
return [
|
||||
k
|
||||
for k, v in required.items()
|
||||
if ex_mod.has.get(k) is not True
|
||||
and (len(v) == 0 or not (all(ex_mod.has.get(x) for x in v)))
|
||||
]
|
||||
|
||||
|
||||
def validate_exchange(exchange: str) -> tuple[bool, str, str, ccxt.Exchange | None]:
|
||||
"""
|
||||
returns: can_use, reason, exchange_object
|
||||
with Reason including both missing and missing_opt
|
||||
@@ -64,36 +80,38 @@ def validate_exchange(exchange: str) -> tuple[bool, str, ccxt.Exchange | None]:
|
||||
ex_mod = getattr(ccxt.async_support, exchange.lower())()
|
||||
|
||||
if not ex_mod or not ex_mod.has:
|
||||
return False, "", None
|
||||
return False, "", "", None
|
||||
|
||||
result = True
|
||||
reason = ""
|
||||
missing = [
|
||||
k
|
||||
for k, v in EXCHANGE_HAS_REQUIRED.items()
|
||||
if ex_mod.has.get(k) is not True and not (all(ex_mod.has.get(x) for x in v))
|
||||
]
|
||||
reasons = []
|
||||
reasons_fut = ""
|
||||
missing = _exchange_has_helper(ex_mod, EXCHANGE_HAS_REQUIRED)
|
||||
if missing:
|
||||
result = False
|
||||
reason += f"missing: {', '.join(missing)}"
|
||||
reasons.append(f"missing: {', '.join(missing)}")
|
||||
|
||||
missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)]
|
||||
missing_opt = _exchange_has_helper(ex_mod, EXCHANGE_HAS_OPTIONAL)
|
||||
|
||||
missing_futures = _exchange_has_helper(ex_mod, EXCHANGE_HAS_OPTIONAL_FUTURES)
|
||||
|
||||
if exchange.lower() in BAD_EXCHANGES:
|
||||
result = False
|
||||
reason = BAD_EXCHANGES.get(exchange.lower(), "")
|
||||
reasons.append(BAD_EXCHANGES.get(exchange.lower(), ""))
|
||||
|
||||
if missing_opt:
|
||||
reason += f"{'. ' if reason else ''}missing opt: {', '.join(missing_opt)}. "
|
||||
reasons.append(f"missing opt: {', '.join(missing_opt)}")
|
||||
|
||||
return result, reason, ex_mod
|
||||
if missing_futures:
|
||||
reasons_fut = f"missing futures opt: {', '.join(missing_futures)}"
|
||||
|
||||
return result, "; ".join(reasons), reasons_fut, ex_mod
|
||||
|
||||
|
||||
def _build_exchange_list_entry(
|
||||
exchange_name: str, exchangeClasses: dict[str, Any]
|
||||
) -> ValidExchangesType:
|
||||
exchange_name = exchange_name.lower()
|
||||
valid, comment, ex_mod = validate_exchange(exchange_name)
|
||||
valid, comment, comment_fut, ex_mod = validate_exchange(exchange_name)
|
||||
mapped_exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name).lower()
|
||||
is_alias = getattr(ex_mod, "alias", False)
|
||||
result: ValidExchangesType = {
|
||||
@@ -102,6 +120,7 @@ def _build_exchange_list_entry(
|
||||
"valid": valid,
|
||||
"supported": mapped_exchange_name in SUPPORTED_EXCHANGES and not is_alias,
|
||||
"comment": comment,
|
||||
"comment_futures": comment_fut,
|
||||
"dex": getattr(ex_mod, "dex", False),
|
||||
"is_alias": is_alias,
|
||||
"alias_for": inspect.getmro(ex_mod.__class__)[1]().id
|
||||
|
||||
@@ -10,8 +10,7 @@ from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||
from freqtrade.misc import safe_value_fallback2
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -30,6 +29,8 @@ class Gate(Exchange):
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stop_price_param": "stopPrice",
|
||||
"stop_price_prop": "stopPrice",
|
||||
"stoploss_query_requires_stop_flag": True,
|
||||
"stoploss_algo_order_info_id": "fired_order_id",
|
||||
"l2_limit_upper": 1000,
|
||||
"marketOrderRequiresPrice": True,
|
||||
"trades_has_history": False, # Endpoint would support this - but ccxt doesn't.
|
||||
@@ -42,6 +43,7 @@ class Gate(Exchange):
|
||||
"stop_price_type_field": "price_type",
|
||||
"l2_limit_upper": 300,
|
||||
"stoploss_blocks_assets": False,
|
||||
"stoploss_algo_order_info_id": "trade_id",
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: 0,
|
||||
PriceType.MARK: 1,
|
||||
@@ -129,28 +131,3 @@ class Gate(Exchange):
|
||||
"rate": pair_fees[takerOrMaker],
|
||||
}
|
||||
return trades
|
||||
|
||||
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
||||
return safe_value_fallback2(order, order, "id_stop", "id")
|
||||
|
||||
def fetch_stoploss_order(
|
||||
self, order_id: str, pair: str, params: dict | None = None
|
||||
) -> CcxtOrder:
|
||||
order = self.fetch_order(order_id=order_id, pair=pair, params={"stop": True})
|
||||
if order.get("status", "open") == "closed":
|
||||
# Places a real order - which we need to fetch explicitly.
|
||||
val = "trade_id" if self.trading_mode == TradingMode.FUTURES else "fired_order_id"
|
||||
|
||||
if new_orderid := order.get("info", {}).get(val):
|
||||
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
|
||||
order1["id_stop"] = order1["id"]
|
||||
order1["id"] = order_id
|
||||
order1["type"] = "stoploss"
|
||||
order1["stopPrice"] = order.get("stopPrice")
|
||||
order1["status_stop"] = "triggered"
|
||||
|
||||
return order1
|
||||
return order
|
||||
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
||||
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
|
||||
|
||||
@@ -7,9 +7,10 @@ from typing import Any
|
||||
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.enums import MarginMode, TradingMode
|
||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.enums.runmode import NON_UTIL_MODES
|
||||
from freqtrade.exceptions import ConfigurationError, ExchangeError, OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||
from freqtrade.exchange.exchange_types import CcxtBalances, CcxtOrder, CcxtPosition, FtHas
|
||||
from freqtrade.util.datetime_helpers import dt_from_ts
|
||||
|
||||
|
||||
@@ -37,9 +38,9 @@ class Hyperliquid(Exchange):
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stoploss_blocks_assets": False,
|
||||
"stop_price_prop": "stopPrice",
|
||||
"funding_fee_timeframe": "1h",
|
||||
"funding_fee_candle_limit": 500,
|
||||
"uses_leverage_tiers": False,
|
||||
"mark_ohlcv_price": "futures",
|
||||
}
|
||||
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
@@ -57,12 +58,108 @@ class Hyperliquid(Exchange):
|
||||
config.update(super()._ccxt_config)
|
||||
return config
|
||||
|
||||
def _get_configured_hip3_dexes(self) -> list[str]:
|
||||
"""Get list of configured HIP-3 DEXes."""
|
||||
return self._config.get("exchange", {}).get("hip3_dexes", [])
|
||||
|
||||
def validate_config(self, config: dict) -> None:
|
||||
"""Validate HIP-3 configuration at bot startup."""
|
||||
super().validate_config(config)
|
||||
configured = self._get_configured_hip3_dexes()
|
||||
if not configured or not self.markets:
|
||||
return
|
||||
if self.trading_mode != TradingMode.FUTURES:
|
||||
if configured:
|
||||
raise ConfigurationError(
|
||||
"HIP-3 DEXes are only supported in FUTURES trading mode. "
|
||||
"Please update your configuration!"
|
||||
)
|
||||
return
|
||||
if configured and self.margin_mode != MarginMode.ISOLATED:
|
||||
raise ConfigurationError(
|
||||
"HIP-3 DEXes require 'isolated' margin mode. "
|
||||
f"Current margin mode: '{self.margin_mode.value}'. "
|
||||
"Please update your configuration!"
|
||||
)
|
||||
|
||||
available = {
|
||||
m.get("info", {}).get("dex")
|
||||
for m in self.get_markets(
|
||||
quote_currencies=[self._config["stake_currency"]],
|
||||
tradable_only=True,
|
||||
active_only=True,
|
||||
).values()
|
||||
if m.get("info", {}).get("hip3")
|
||||
}
|
||||
available.discard(None)
|
||||
|
||||
invalid = set(configured) - available
|
||||
if invalid:
|
||||
raise ConfigurationError(
|
||||
f"Invalid HIP-3 DEXes configured: {sorted(invalid)}. "
|
||||
f"Available DEXes matching your stake currency ({self._config['stake_currency']}): "
|
||||
f"{sorted(available)}. "
|
||||
f"Check your 'hip3_dexes' configuration!"
|
||||
)
|
||||
|
||||
def market_is_tradable(self, market: dict[str, Any]) -> bool:
|
||||
"""Check if market is tradable, including HIP-3 markets."""
|
||||
parent_check = super().market_is_tradable(market)
|
||||
|
||||
# Exclude hip3 markets for now - which have the format XYZ:GOOGL/USDT:USDT -
|
||||
# and XYZ:GOOGL as base
|
||||
return parent_check and ":" not in market["base"]
|
||||
market_info = market.get("info", {})
|
||||
if market_info.get("hip3") and self._config["runmode"] in NON_UTIL_MODES:
|
||||
configured = self._get_configured_hip3_dexes()
|
||||
if not configured:
|
||||
return False
|
||||
|
||||
market_dex = market_info.get("dex")
|
||||
return parent_check and market_dex in configured
|
||||
|
||||
return parent_check
|
||||
|
||||
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||
"""Fetch balances from default DEX and HIP-3 DEXes needed by tradable pairs.
|
||||
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.
|
||||
"""
|
||||
balances = super().get_balances()
|
||||
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():
|
||||
if currency in ["info", "free", "used", "total", "datetime", "timestamp"]:
|
||||
continue
|
||||
|
||||
if currency not in balances:
|
||||
balances[currency] = amount_info
|
||||
else:
|
||||
balances[currency]["free"] += amount_info["free"]
|
||||
balances[currency]["used"] += amount_info["used"]
|
||||
balances[currency]["total"] += amount_info["total"]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Could not fetch balance for HIP-3 DEX '{dex}': {e}")
|
||||
|
||||
if dexes:
|
||||
self._log_exchange_response("fetch_balance", balances, add_info="combined")
|
||||
return balances
|
||||
|
||||
def fetch_positions(
|
||||
self, pair: str | None = None, params: dict | None = None
|
||||
) -> list[CcxtPosition]:
|
||||
"""Fetch positions from default DEX and HIP-3 DEXes needed by tradable pairs."""
|
||||
positions = super().fetch_positions(pair)
|
||||
dexes = self._get_configured_hip3_dexes()
|
||||
for dex in dexes:
|
||||
try:
|
||||
positions.extend(super().fetch_positions(pair, params={"dex": dex}))
|
||||
except Exception as e:
|
||||
logger.error(f"Could not fetch positions from HIP-3 DEX '{dex}': {e}")
|
||||
if dexes:
|
||||
self._log_exchange_response("fetch_positions", positions, add_info="combined")
|
||||
return positions
|
||||
|
||||
def get_max_leverage(self, pair: str, stake_amount: float | None) -> float:
|
||||
# There are no leverage tiers
|
||||
|
||||
@@ -35,7 +35,6 @@ class Kraken(Exchange):
|
||||
"trades_pagination_arg": "since",
|
||||
"trades_pagination_overlap": False,
|
||||
"trades_has_history": True,
|
||||
"mark_ohlcv_timeframe": "4h",
|
||||
}
|
||||
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
@@ -71,7 +70,7 @@ class Kraken(Exchange):
|
||||
return consolidated
|
||||
|
||||
@retrier
|
||||
def get_balances(self) -> CcxtBalances:
|
||||
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||
if self._config["dry_run"]:
|
||||
return {}
|
||||
|
||||
|
||||
@@ -44,8 +44,9 @@ class Kucoin(Exchange):
|
||||
amount: float,
|
||||
rate: float,
|
||||
leverage: float,
|
||||
reduceOnly: bool = False,
|
||||
time_in_force: str = "GTC",
|
||||
reduceOnly: bool = False,
|
||||
initial_order: bool = True,
|
||||
) -> CcxtOrder:
|
||||
res = super().create_order(
|
||||
pair=pair,
|
||||
@@ -56,6 +57,7 @@ class Kucoin(Exchange):
|
||||
leverage=leverage,
|
||||
reduceOnly=reduceOnly,
|
||||
time_in_force=time_in_force,
|
||||
initial_order=initial_order,
|
||||
)
|
||||
# Kucoin returns only the order-id.
|
||||
# ccxt returns status = 'closed' at the moment - which is information ccxt invented.
|
||||
|
||||
@@ -14,7 +14,6 @@ from freqtrade.exceptions import (
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||
from freqtrade.misc import safe_value_fallback2
|
||||
from freqtrade.util import dt_now, dt_ts
|
||||
|
||||
|
||||
@@ -29,10 +28,9 @@ class Okx(Exchange):
|
||||
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
|
||||
"mark_ohlcv_timeframe": "4h",
|
||||
"funding_fee_timeframe": "8h",
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_query_requires_stop_flag": True,
|
||||
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
|
||||
"ws_enabled": True,
|
||||
}
|
||||
@@ -41,8 +39,8 @@ class Okx(Exchange):
|
||||
"stop_price_type_field": "slTriggerPxType",
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: "last",
|
||||
PriceType.MARK: "index",
|
||||
PriceType.INDEX: "mark",
|
||||
PriceType.MARK: "mark",
|
||||
PriceType.INDEX: "index",
|
||||
},
|
||||
"stoploss_blocks_assets": False,
|
||||
"ws_enabled": True,
|
||||
@@ -184,7 +182,10 @@ class Okx(Exchange):
|
||||
return float("inf")
|
||||
|
||||
pair_tiers = self._leverage_tiers[pair]
|
||||
return pair_tiers[-1]["maxNotional"] / leverage
|
||||
last_max_notional = pair_tiers[-1]["maxNotional"]
|
||||
if last_max_notional is None:
|
||||
return float("inf")
|
||||
return last_max_notional / leverage
|
||||
|
||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> dict:
|
||||
params = super()._get_stop_params(side, ordertype, stop_price)
|
||||
@@ -260,21 +261,6 @@ class Okx(Exchange):
|
||||
raise OperationalException(e) from e
|
||||
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
|
||||
|
||||
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
||||
if order.get("type", "") == "stop":
|
||||
return safe_value_fallback2(order, order, "id_stop", "id")
|
||||
return order["id"]
|
||||
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
||||
params1 = {"stop": True}
|
||||
# 'ordType': 'conditional'
|
||||
#
|
||||
return self.cancel_order(
|
||||
order_id=order_id,
|
||||
pair=pair,
|
||||
params=params1,
|
||||
)
|
||||
|
||||
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
|
||||
orders = []
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class BaseClassifierModel(IFreqaiModel):
|
||||
"""
|
||||
Base class for regression type models (e.g. Catboost, LightGBM, XGboost etc.).
|
||||
User *must* inherit from this class and set fit(). See example scripts
|
||||
such as prediction_models/CatboostClassifier.py for guidance.
|
||||
such as prediction_models/XGBoostClassifier.py for guidance.
|
||||
"""
|
||||
|
||||
def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
|
||||
@@ -18,7 +18,7 @@ class BaseRegressionModel(IFreqaiModel):
|
||||
"""
|
||||
Base class for regression type models (e.g. Catboost, LightGBM, XGboost etc.).
|
||||
User *must* inherit from this class and set fit(). See example scripts
|
||||
such as prediction_models/CatboostRegressor.py for guidance.
|
||||
such as prediction_models/XGBoostRegressor.py for guidance.
|
||||
"""
|
||||
|
||||
def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
|
||||
@@ -227,6 +227,10 @@ class IFreqaiModel(ABC):
|
||||
"""
|
||||
while not self._stop_event.is_set():
|
||||
time.sleep(1)
|
||||
|
||||
if not self.train_queue:
|
||||
continue
|
||||
|
||||
pair = self.train_queue[0]
|
||||
|
||||
# ensure pair is available in dp
|
||||
@@ -948,7 +952,7 @@ class IFreqaiModel(ABC):
|
||||
return dk
|
||||
|
||||
# Following methods which are overridden by user made prediction models.
|
||||
# See freqai/prediction_models/CatboostPredictionModel.py for an example.
|
||||
# See freqai/prediction_models/XGBoostRegressor.py for an example.
|
||||
|
||||
@abstractmethod
|
||||
def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
@@ -964,7 +968,7 @@ class IFreqaiModel(ABC):
|
||||
def fit(self, data_dictionary: dict[str, Any], dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
"""
|
||||
Most regressors use the same function names and arguments e.g. user
|
||||
can drop in LGBMRegressor in place of CatBoostRegressor and all data
|
||||
can drop in LGBMRegressor in place of XGBoostRegressor and all data
|
||||
management will be properly handled by Freqai.
|
||||
:param data_dictionary: Dict = the dictionary constructed by DataHandler to hold
|
||||
all the training and test data/labels.
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from catboost import CatBoostClassifier, Pool
|
||||
|
||||
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CatboostClassifier(BaseClassifierModel):
|
||||
"""
|
||||
User created prediction model. The class inherits IFreqaiModel, which
|
||||
means it has full access to all Frequency AI functionality. Typically,
|
||||
users would use this to override the common `fit()`, `train()`, or
|
||||
`predict()` methods to add their custom data handling tools or change
|
||||
various aspects of the training that cannot be configured via the
|
||||
top level config.json file.
|
||||
"""
|
||||
|
||||
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
"""
|
||||
User sets up the training and test data to fit their desired model here
|
||||
:param data_dictionary: the dictionary holding all data for train, test,
|
||||
labels, weights
|
||||
:param dk: The datakitchen object for the current coin/model
|
||||
"""
|
||||
|
||||
train_data = Pool(
|
||||
data=data_dictionary["train_features"],
|
||||
label=data_dictionary["train_labels"],
|
||||
weight=data_dictionary["train_weights"],
|
||||
)
|
||||
if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0:
|
||||
test_data = None
|
||||
else:
|
||||
test_data = Pool(
|
||||
data=data_dictionary["test_features"],
|
||||
label=data_dictionary["test_labels"],
|
||||
weight=data_dictionary["test_weights"],
|
||||
)
|
||||
|
||||
cbr = CatBoostClassifier(
|
||||
allow_writing_files=True,
|
||||
loss_function="MultiClass",
|
||||
train_dir=Path(dk.data_path),
|
||||
**self.model_training_parameters,
|
||||
)
|
||||
|
||||
init_model = self.get_init_model(dk.pair)
|
||||
|
||||
cbr.fit(
|
||||
X=train_data,
|
||||
eval_set=test_data,
|
||||
init_model=init_model,
|
||||
)
|
||||
|
||||
return cbr
|
||||
@@ -1,79 +0,0 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from catboost import CatBoostClassifier, Pool
|
||||
|
||||
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
|
||||
from freqtrade.freqai.base_models.FreqaiMultiOutputClassifier import FreqaiMultiOutputClassifier
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CatboostClassifierMultiTarget(BaseClassifierModel):
|
||||
"""
|
||||
User created prediction model. The class inherits IFreqaiModel, which
|
||||
means it has full access to all Frequency AI functionality. Typically,
|
||||
users would use this to override the common `fit()`, `train()`, or
|
||||
`predict()` methods to add their custom data handling tools or change
|
||||
various aspects of the training that cannot be configured via the
|
||||
top level config.json file.
|
||||
"""
|
||||
|
||||
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
"""
|
||||
User sets up the training and test data to fit their desired model here
|
||||
:param data_dictionary: the dictionary holding all data for train, test,
|
||||
labels, weights
|
||||
:param dk: The datakitchen object for the current coin/model
|
||||
"""
|
||||
|
||||
cbc = CatBoostClassifier(
|
||||
allow_writing_files=True,
|
||||
loss_function="MultiClass",
|
||||
train_dir=Path(dk.data_path),
|
||||
**self.model_training_parameters,
|
||||
)
|
||||
|
||||
X = data_dictionary["train_features"]
|
||||
y = data_dictionary["train_labels"]
|
||||
|
||||
sample_weight = data_dictionary["train_weights"]
|
||||
|
||||
eval_sets = [None] * y.shape[1]
|
||||
|
||||
if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) != 0:
|
||||
eval_sets = [None] * data_dictionary["test_labels"].shape[1]
|
||||
|
||||
for i in range(data_dictionary["test_labels"].shape[1]):
|
||||
eval_sets[i] = Pool(
|
||||
data=data_dictionary["test_features"],
|
||||
label=data_dictionary["test_labels"].iloc[:, i],
|
||||
weight=data_dictionary["test_weights"],
|
||||
)
|
||||
|
||||
init_model = self.get_init_model(dk.pair)
|
||||
|
||||
if init_model:
|
||||
init_models = init_model.estimators_
|
||||
else:
|
||||
init_models = [None] * y.shape[1]
|
||||
|
||||
fit_params = []
|
||||
for i in range(len(eval_sets)):
|
||||
fit_params.append(
|
||||
{
|
||||
"eval_set": eval_sets[i],
|
||||
"init_model": init_models[i],
|
||||
}
|
||||
)
|
||||
|
||||
model = FreqaiMultiOutputClassifier(estimator=cbc)
|
||||
thread_training = self.freqai_info.get("multitarget_parallel_training", False)
|
||||
if thread_training:
|
||||
model.n_jobs = y.shape[1]
|
||||
model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params)
|
||||
|
||||
return model
|
||||
@@ -1,60 +0,0 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from catboost import CatBoostRegressor, Pool
|
||||
|
||||
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CatboostRegressor(BaseRegressionModel):
|
||||
"""
|
||||
User created prediction model. The class inherits IFreqaiModel, which
|
||||
means it has full access to all Frequency AI functionality. Typically,
|
||||
users would use this to override the common `fit()`, `train()`, or
|
||||
`predict()` methods to add their custom data handling tools or change
|
||||
various aspects of the training that cannot be configured via the
|
||||
top level config.json file.
|
||||
"""
|
||||
|
||||
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
"""
|
||||
User sets up the training and test data to fit their desired model here
|
||||
:param data_dictionary: the dictionary holding all data for train, test,
|
||||
labels, weights
|
||||
:param dk: The datakitchen object for the current coin/model
|
||||
"""
|
||||
|
||||
train_data = Pool(
|
||||
data=data_dictionary["train_features"],
|
||||
label=data_dictionary["train_labels"],
|
||||
weight=data_dictionary["train_weights"],
|
||||
)
|
||||
if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0:
|
||||
test_data = None
|
||||
else:
|
||||
test_data = Pool(
|
||||
data=data_dictionary["test_features"],
|
||||
label=data_dictionary["test_labels"],
|
||||
weight=data_dictionary["test_weights"],
|
||||
)
|
||||
|
||||
init_model = self.get_init_model(dk.pair)
|
||||
|
||||
model = CatBoostRegressor(
|
||||
allow_writing_files=True,
|
||||
train_dir=Path(dk.data_path),
|
||||
**self.model_training_parameters,
|
||||
)
|
||||
|
||||
model.fit(
|
||||
X=train_data,
|
||||
eval_set=test_data,
|
||||
init_model=init_model,
|
||||
)
|
||||
|
||||
return model
|
||||
@@ -1,78 +0,0 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from catboost import CatBoostRegressor, Pool
|
||||
|
||||
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
|
||||
from freqtrade.freqai.base_models.FreqaiMultiOutputRegressor import FreqaiMultiOutputRegressor
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CatboostRegressorMultiTarget(BaseRegressionModel):
|
||||
"""
|
||||
User created prediction model. The class inherits IFreqaiModel, which
|
||||
means it has full access to all Frequency AI functionality. Typically,
|
||||
users would use this to override the common `fit()`, `train()`, or
|
||||
`predict()` methods to add their custom data handling tools or change
|
||||
various aspects of the training that cannot be configured via the
|
||||
top level config.json file.
|
||||
"""
|
||||
|
||||
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
"""
|
||||
User sets up the training and test data to fit their desired model here
|
||||
:param data_dictionary: the dictionary holding all data for train, test,
|
||||
labels, weights
|
||||
:param dk: The datakitchen object for the current coin/model
|
||||
"""
|
||||
|
||||
cbr = CatBoostRegressor(
|
||||
allow_writing_files=True,
|
||||
train_dir=Path(dk.data_path),
|
||||
**self.model_training_parameters,
|
||||
)
|
||||
|
||||
X = data_dictionary["train_features"]
|
||||
y = data_dictionary["train_labels"]
|
||||
|
||||
sample_weight = data_dictionary["train_weights"]
|
||||
|
||||
eval_sets = [None] * y.shape[1]
|
||||
|
||||
if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) != 0:
|
||||
eval_sets = [None] * data_dictionary["test_labels"].shape[1]
|
||||
|
||||
for i in range(data_dictionary["test_labels"].shape[1]):
|
||||
eval_sets[i] = Pool(
|
||||
data=data_dictionary["test_features"],
|
||||
label=data_dictionary["test_labels"].iloc[:, i],
|
||||
weight=data_dictionary["test_weights"],
|
||||
)
|
||||
|
||||
init_model = self.get_init_model(dk.pair)
|
||||
|
||||
if init_model:
|
||||
init_models = init_model.estimators_
|
||||
else:
|
||||
init_models = [None] * y.shape[1]
|
||||
|
||||
fit_params = []
|
||||
for i in range(len(eval_sets)):
|
||||
fit_params.append(
|
||||
{
|
||||
"eval_set": eval_sets[i],
|
||||
"init_model": init_models[i],
|
||||
}
|
||||
)
|
||||
|
||||
model = FreqaiMultiOutputRegressor(estimator=cbr)
|
||||
thread_training = self.freqai_info.get("multitarget_parallel_training", False)
|
||||
if thread_training:
|
||||
model.n_jobs = y.shape[1]
|
||||
model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params)
|
||||
|
||||
return model
|
||||
@@ -97,7 +97,7 @@ def plot_feature_importance(
|
||||
"""
|
||||
Plot Best and worst features by importance for a single sub-train.
|
||||
:param model: Any = A model which was `fit` using a common library
|
||||
such as catboost or lightgbm
|
||||
such as XGBoost or lightgbm
|
||||
:param pair: str = pair e.g. BTC/USD
|
||||
:param dk: FreqaiDataKitchen = non-persistent data container for current coin/loop
|
||||
:param count_max: int = the amount of features to be loaded per column
|
||||
@@ -115,6 +115,8 @@ def plot_feature_importance(
|
||||
for label in models:
|
||||
mdl = models[label]
|
||||
if "catboost.core" in str(mdl.__class__):
|
||||
# CatBoost is no longer actively supported since 2025.12
|
||||
# However users can still use it in their custom models
|
||||
feature_importance = mdl.get_feature_importance()
|
||||
elif "lightgbm.sklearn" in str(mdl.__class__):
|
||||
feature_importance = mdl.feature_importances_
|
||||
|
||||
+41
-23
@@ -937,6 +937,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
reduceOnly=False,
|
||||
time_in_force=time_in_force,
|
||||
leverage=leverage,
|
||||
initial_order=trade is None,
|
||||
)
|
||||
order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested)
|
||||
order_obj.ft_order_tag = enter_tag
|
||||
@@ -1063,7 +1064,16 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
return True
|
||||
|
||||
def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade:
|
||||
def cancel_stoploss_on_exchange(self, trade: Trade, allow_nonblocking: bool = False) -> Trade:
|
||||
"""
|
||||
Cancels on exchange stoploss orders for the given trade.
|
||||
:param trade: Trade for which to cancel stoploss order
|
||||
:param allow_nonblocking: If True, will skip cancelling stoploss on exchange
|
||||
if the exchange supports blocking stoploss orders.
|
||||
"""
|
||||
if allow_nonblocking and not self.exchange.get_option("stoploss_blocks_assets", True):
|
||||
logger.info(f"Skipping cancelling stoploss on exchange for {trade}.")
|
||||
return trade
|
||||
# First cancelling stoploss on exchange ...
|
||||
for oslo in trade.open_sl_orders:
|
||||
try:
|
||||
@@ -2002,14 +2012,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
def _safe_exit_amount(self, trade: Trade, pair: str, amount: float) -> float:
|
||||
"""
|
||||
Get sellable amount.
|
||||
Get exitable amount.
|
||||
Should be trade.amount - but will fall back to the available amount if necessary.
|
||||
This should cover cases where get_real_amount() was not able to update the amount
|
||||
for whatever reason.
|
||||
:param trade: Trade we're working with
|
||||
:param pair: Pair we're trying to sell
|
||||
:param pair: Pair we're trying to exit
|
||||
:param amount: amount we expect to be available
|
||||
:return: amount to sell
|
||||
:return: amount to exit
|
||||
:raise: DependencyException: if available balance is not within 2% of the available amount.
|
||||
"""
|
||||
# Update wallets to ensure amounts tied up in a stoploss is now free!
|
||||
@@ -2045,11 +2055,12 @@ class FreqtradeBot(LoggingMixin):
|
||||
exit_tag: str | None = None,
|
||||
ordertype: str | None = None,
|
||||
sub_trade_amt: float | None = None,
|
||||
skip_custom_exit_price: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Executes a trade exit for the given trade and limit
|
||||
:param trade: Trade instance
|
||||
:param limit: limit rate for the sell order
|
||||
:param limit: limit rate for the exit order
|
||||
:param exit_check: CheckTuple with signal and reason
|
||||
:return: True if it succeeds False
|
||||
"""
|
||||
@@ -2071,29 +2082,33 @@ class FreqtradeBot(LoggingMixin):
|
||||
):
|
||||
exit_type = "stoploss"
|
||||
|
||||
order_type = (
|
||||
(ordertype or self.strategy.order_types[exit_type])
|
||||
if exit_check.exit_type != ExitType.EMERGENCY_EXIT
|
||||
else self.strategy.order_types.get("emergency_exit", "market")
|
||||
)
|
||||
|
||||
# set custom_exit_price if available
|
||||
proposed_limit_rate = limit
|
||||
custom_exit_price = limit
|
||||
|
||||
current_profit = trade.calc_profit_ratio(limit)
|
||||
custom_exit_price = strategy_safe_wrapper(
|
||||
self.strategy.custom_exit_price, default_retval=proposed_limit_rate
|
||||
)(
|
||||
pair=trade.pair,
|
||||
trade=trade,
|
||||
current_time=datetime.now(UTC),
|
||||
proposed_rate=proposed_limit_rate,
|
||||
current_profit=current_profit,
|
||||
exit_tag=exit_reason,
|
||||
)
|
||||
if order_type == "limit" and not skip_custom_exit_price:
|
||||
custom_exit_price = strategy_safe_wrapper(
|
||||
self.strategy.custom_exit_price, default_retval=proposed_limit_rate
|
||||
)(
|
||||
pair=trade.pair,
|
||||
trade=trade,
|
||||
current_time=datetime.now(UTC),
|
||||
proposed_rate=proposed_limit_rate,
|
||||
current_profit=current_profit,
|
||||
exit_tag=exit_reason,
|
||||
)
|
||||
|
||||
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
|
||||
|
||||
# First cancelling stoploss on exchange ...
|
||||
trade = self.cancel_stoploss_on_exchange(trade)
|
||||
|
||||
order_type = ordertype or self.strategy.order_types[exit_type]
|
||||
if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
|
||||
# Emergency sells (default to market!)
|
||||
order_type = self.strategy.order_types.get("emergency_exit", "market")
|
||||
trade = self.cancel_stoploss_on_exchange(trade, allow_nonblocking=True)
|
||||
|
||||
amount = self._safe_exit_amount(trade, trade.pair, sub_trade_amt or trade.amount)
|
||||
time_in_force = self.strategy.order_time_in_force["exit"]
|
||||
@@ -2121,7 +2136,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
return False
|
||||
|
||||
try:
|
||||
# Execute sell and update trade record
|
||||
# Execute exit and update trade record
|
||||
order = self.exchange.create_order(
|
||||
pair=trade.pair,
|
||||
ordertype=order_type,
|
||||
@@ -2131,6 +2146,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
leverage=trade.leverage,
|
||||
reduceOnly=self.trading_mode == TradingMode.FUTURES,
|
||||
time_in_force=time_in_force,
|
||||
initial_order=False,
|
||||
)
|
||||
except InsufficientFundsError as e:
|
||||
logger.warning(f"Unable to place order {e}.")
|
||||
@@ -2148,7 +2164,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.exit_reason = exit_reason
|
||||
|
||||
self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
|
||||
# In case of market sell orders the order can be closed immediately
|
||||
# In case of market exit orders the order can be closed immediately
|
||||
if order.get("status", "unknown") in ("closed", "expired"):
|
||||
self.update_trade_state(trade, order_obj.order_id, order)
|
||||
Trade.commit()
|
||||
@@ -2378,6 +2394,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.strategy.ft_stoploss_adjust(
|
||||
current_rate, trade, datetime.now(UTC), profit, 0, after_fill=True
|
||||
)
|
||||
if not trade.is_open:
|
||||
self.cancel_stoploss_on_exchange(trade)
|
||||
# Updating wallets when order is closed
|
||||
self.wallets.update()
|
||||
return trade
|
||||
|
||||
@@ -6,25 +6,36 @@ from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class _BaseAnnotationType(TypedDict, total=False):
|
||||
start: str | datetime
|
||||
end: str | datetime
|
||||
y_start: float
|
||||
y_end: float
|
||||
color: str
|
||||
label: str
|
||||
z_level: int
|
||||
|
||||
|
||||
class AreaAnnotationType(_BaseAnnotationType, total=False):
|
||||
class _Base2DAnnotationType(_BaseAnnotationType, total=False):
|
||||
start: str | datetime
|
||||
end: str | datetime
|
||||
y_start: float
|
||||
y_end: float
|
||||
|
||||
|
||||
class AreaAnnotationType(_Base2DAnnotationType, total=False):
|
||||
type: Required[Literal["area"]]
|
||||
|
||||
|
||||
class LineAnnotationType(_BaseAnnotationType, total=False):
|
||||
class LineAnnotationType(_Base2DAnnotationType, total=False):
|
||||
type: Required[Literal["line"]]
|
||||
width: int
|
||||
line_style: Literal["solid", "dashed", "dotted"]
|
||||
|
||||
|
||||
AnnotationType = AreaAnnotationType | LineAnnotationType
|
||||
class PointAnnotationType(_BaseAnnotationType, total=False):
|
||||
type: Required[Literal["point"]]
|
||||
x: str | datetime
|
||||
y: float
|
||||
size: int
|
||||
shape: Literal["circle", "rect", "roundRect", "triangle", "pin", "arrow", "none"]
|
||||
|
||||
|
||||
AnnotationType = AreaAnnotationType | LineAnnotationType | PointAnnotationType
|
||||
|
||||
AnnotationTypeTA: TypeAdapter[AnnotationType] = TypeAdapter(AnnotationType)
|
||||
|
||||
@@ -14,6 +14,7 @@ class ValidExchangesType(TypedDict):
|
||||
valid: bool
|
||||
supported: bool
|
||||
comment: str
|
||||
comment_futures: str
|
||||
dex: bool
|
||||
is_alias: bool
|
||||
alias_for: str | None
|
||||
|
||||
@@ -374,6 +374,7 @@ class Backtesting:
|
||||
timerange=self.timerange,
|
||||
startup_candles=0,
|
||||
fail_without_data=True,
|
||||
fill_up_missing=False,
|
||||
data_format=self.config["dataformat_ohlcv"],
|
||||
candle_type=CandleType.FUNDING_RATE,
|
||||
)
|
||||
@@ -438,6 +439,8 @@ class Backtesting:
|
||||
PairLocks.reset_locks()
|
||||
Trade.reset_trades()
|
||||
CustomDataWrapper.reset_custom_data()
|
||||
# Ensure logging is disabled in other processes during hyperopt
|
||||
LoggingMixin.show_output = False
|
||||
self.rejected_trades = 0
|
||||
self.timedout_entry_orders = 0
|
||||
self.timedout_exit_orders = 0
|
||||
@@ -602,8 +605,6 @@ class Backtesting:
|
||||
trade_dur: int,
|
||||
) -> float:
|
||||
is_short = trade.is_short or False
|
||||
leverage = trade.leverage or 1.0
|
||||
side_1 = -1 if is_short else 1
|
||||
roi_entry, roi = self.strategy.min_roi_reached_entry(
|
||||
trade, # type: ignore[arg-type]
|
||||
trade_dur,
|
||||
@@ -616,10 +617,7 @@ class Backtesting:
|
||||
# - we'll use open instead of close
|
||||
return row[OPEN_IDX]
|
||||
|
||||
# - (Expected abs profit - open_rate - open_fee) / (fee_close -1)
|
||||
roi_rate = trade.open_rate * roi / leverage
|
||||
open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open)
|
||||
close_rate = -(roi_rate + open_fee_rate) / ((trade.fee_close or 0.0) - side_1 * 1)
|
||||
close_rate = trade.calc_close_rate_for_roi(roi)
|
||||
if is_short:
|
||||
is_new_roi = row[OPEN_IDX] < close_rate
|
||||
else:
|
||||
|
||||
@@ -21,6 +21,11 @@ def logging_mp_setup(log_queue: Queue, verbosity: int):
|
||||
root = logging.getLogger()
|
||||
root.setLevel(verbosity)
|
||||
root.addHandler(h)
|
||||
# Disable freqtrade logging outside of the main process
|
||||
# This only leaves logging from the strategy (unless it's prefixed with "freqtrade.")
|
||||
# and eventually from other libraries.
|
||||
if verbosity > logging.DEBUG:
|
||||
logging.getLogger("freqtrade").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def logging_mp_handle(q: Queue):
|
||||
|
||||
@@ -6,7 +6,7 @@ and will be sent to the hyperopt worker processes.
|
||||
import logging
|
||||
import sys
|
||||
import warnings
|
||||
from datetime import UTC, datetime
|
||||
from datetime import datetime
|
||||
from multiprocessing import Manager
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -42,6 +42,7 @@ from freqtrade.optimize.space import (
|
||||
ft_IntDistribution,
|
||||
)
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||
from freqtrade.util import dt_now
|
||||
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
|
||||
|
||||
|
||||
@@ -269,7 +270,7 @@ class HyperOptimizer:
|
||||
Keep this function as optimized as possible!
|
||||
"""
|
||||
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
|
||||
backtest_start_time = datetime.now(UTC)
|
||||
backtest_start_time = dt_now()
|
||||
|
||||
for attr_name, attr in self.backtesting.strategy.enumerate_parameters():
|
||||
if attr.in_space and attr.optimize:
|
||||
@@ -320,7 +321,7 @@ class HyperOptimizer:
|
||||
bt_results = self.backtesting.backtest(
|
||||
processed=processed, start_date=self.min_date, end_date=self.max_date
|
||||
)
|
||||
backtest_end_time = datetime.now(UTC)
|
||||
backtest_end_time = dt_now()
|
||||
bt_results.update(
|
||||
{
|
||||
"backtest_start_time": int(backtest_start_time.timestamp()),
|
||||
|
||||
@@ -48,7 +48,7 @@ from freqtrade.leverage import interest
|
||||
from freqtrade.misc import safe_value_fallback
|
||||
from freqtrade.persistence.base import ModelBase, SessionType
|
||||
from freqtrade.persistence.custom_data import CustomDataWrapper, _CustomData
|
||||
from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts, dt_ts_none
|
||||
from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts, dt_ts_none, round_value
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -654,9 +654,10 @@ class LocalTrade:
|
||||
)
|
||||
|
||||
return (
|
||||
f"Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, "
|
||||
f"is_short={self.is_short or False}, leverage={self.leverage or 1.0}, "
|
||||
f"open_rate={self.open_rate:.8f}, open_since={open_since})"
|
||||
f"Trade(id={self.id}, pair={self.pair}, amount={round_value(self.amount, 8)}, "
|
||||
f"is_short={self.is_short or False}, "
|
||||
f"leverage={round_value(self.leverage or 1.0, 1)}, "
|
||||
f"open_rate={round_value(self.open_rate, 8)}, open_since={open_since})"
|
||||
)
|
||||
|
||||
def to_json(self, minified: bool = False) -> dict[str, Any]:
|
||||
@@ -1207,6 +1208,35 @@ class LocalTrade:
|
||||
|
||||
return float(f"{profit_ratio:.8f}")
|
||||
|
||||
def calc_close_rate_for_roi(self, target_roi: float) -> float:
|
||||
"""
|
||||
Calculate the required close price to reach a target ROI.
|
||||
Must match the logic used in `calc_profit_ratio()`.
|
||||
|
||||
:param target_roi: The desired return on investment (as a decimal, e.g., 0.05 for 5%)
|
||||
:return: Close price (rate) required to achieve the target ROI
|
||||
"""
|
||||
leverage = float(self.leverage or 1.0)
|
||||
deleveraged_roi = float(target_roi) / leverage
|
||||
|
||||
open_value = self._calc_open_trade_value(self.amount, self.open_rate)
|
||||
|
||||
# The ROI formula uses close_value(rate), which depends on trading mode:
|
||||
# - SPOT: linear in rate, adjusted by close fee
|
||||
# - MARGIN: same, but long subtracts interest, short increases amount
|
||||
# - FUTURES: adds/subtracts funding to/from close value
|
||||
# All cases are affine in rate:
|
||||
# close_value(rate) = a * rate + b
|
||||
# We extract a and b by probing close_value at rate = 0 and 1.
|
||||
value_at_0 = self.calc_close_trade_value(0.0)
|
||||
value_at_1 = self.calc_close_trade_value(1.0)
|
||||
alpha = value_at_1 - value_at_0
|
||||
beta = value_at_0
|
||||
|
||||
s = -1.0 if self.is_short else 1.0
|
||||
adj = 1.0 + (deleveraged_roi / s)
|
||||
return (adj * open_value - beta) / alpha
|
||||
|
||||
def recalc_trade_from_orders(self, *, is_closing: bool = False):
|
||||
ZERO = FtPrecise(0.0)
|
||||
current_amount = FtPrecise(0.0)
|
||||
|
||||
@@ -53,7 +53,7 @@ class PercentChangePairList(IPairList):
|
||||
self._sort_direction: str | None = self._pairlistconfig.get("sort_direction", "desc")
|
||||
self._def_candletype = self._config["candle_type_def"]
|
||||
|
||||
if (self._lookback_days > 0) & (self._lookback_period > 0):
|
||||
if (self._lookback_days > 0) and (self._lookback_period > 0):
|
||||
raise OperationalException(
|
||||
"Ambiguous configuration: lookback_days and lookback_period both set in pairlist "
|
||||
"config. Please set lookback_days only or lookback_period and lookback_timeframe "
|
||||
@@ -70,7 +70,7 @@ class PercentChangePairList(IPairList):
|
||||
_tf_in_sec = self._tf_in_min * 60
|
||||
|
||||
# whether to use range lookback or not
|
||||
self._use_range = (self._tf_in_min > 0) & (self._lookback_period > 0)
|
||||
self._use_range = (self._tf_in_min > 0) and (self._lookback_period > 0)
|
||||
|
||||
if self._use_range & (self._refresh_period < _tf_in_sec):
|
||||
raise OperationalException(
|
||||
@@ -84,9 +84,9 @@ class PercentChangePairList(IPairList):
|
||||
and self._exchange.get_option("tickers_have_percentage")
|
||||
):
|
||||
raise OperationalException(
|
||||
"Exchange does not support dynamic whitelist in this configuration. "
|
||||
"Please edit your config and either remove PercentChangePairList, "
|
||||
"or switch to using candles. and restart the bot."
|
||||
f"Exchange {self._exchange.name} does not support dynamic whitelist in this "
|
||||
"configuration. Please edit your config and either remove PercentChangePairList, "
|
||||
"or switch to using candles and restart the bot."
|
||||
)
|
||||
|
||||
candle_limit = self._exchange.ohlcv_candle_limit(
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
from datetime import timedelta
|
||||
from typing import Any, Literal
|
||||
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.constants import DOCS_LINK, ListPairsWithTimeframes
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||
from freqtrade.exchange.exchange_types import Tickers
|
||||
@@ -35,19 +35,19 @@ class VolumePairList(IPairList):
|
||||
'for "pairlist.config.number_assets"'
|
||||
)
|
||||
|
||||
self._stake_currency = self._config["stake_currency"]
|
||||
self._number_pairs = self._pairlistconfig["number_assets"]
|
||||
self._stake_currency: str = self._config["stake_currency"]
|
||||
self._number_pairs: int = self._pairlistconfig["number_assets"]
|
||||
self._sort_key: Literal["quoteVolume"] = self._pairlistconfig.get("sort_key", "quoteVolume")
|
||||
self._min_value = self._pairlistconfig.get("min_value", 0)
|
||||
self._max_value = self._pairlistconfig.get("max_value", None)
|
||||
self._min_value: float | None = self._pairlistconfig.get("min_value", 0)
|
||||
self._max_value: float | None = self._pairlistconfig.get("max_value", None)
|
||||
self._refresh_period = self._pairlistconfig.get("refresh_period", 1800)
|
||||
self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1, ttl=self._refresh_period)
|
||||
self._lookback_days = self._pairlistconfig.get("lookback_days", 0)
|
||||
self._lookback_timeframe = self._pairlistconfig.get("lookback_timeframe", "1d")
|
||||
self._lookback_period = self._pairlistconfig.get("lookback_period", 0)
|
||||
self._lookback_days: int = self._pairlistconfig.get("lookback_days", 0)
|
||||
self._lookback_timeframe: str = self._pairlistconfig.get("lookback_timeframe", "1d")
|
||||
self._lookback_period: int = self._pairlistconfig.get("lookback_period", 0)
|
||||
self._def_candletype = self._config["candle_type_def"]
|
||||
|
||||
if (self._lookback_days > 0) & (self._lookback_period > 0):
|
||||
if (self._lookback_days > 0) and (self._lookback_period > 0):
|
||||
raise OperationalException(
|
||||
"Ambiguous configuration: lookback_days and lookback_period both set in pairlist "
|
||||
"config. Please set lookback_days only or lookback_period and lookback_timeframe "
|
||||
@@ -64,9 +64,9 @@ class VolumePairList(IPairList):
|
||||
_tf_in_sec = self._tf_in_min * 60
|
||||
|
||||
# whether to use range lookback or not
|
||||
self._use_range = (self._tf_in_min > 0) & (self._lookback_period > 0)
|
||||
self._use_range = (self._tf_in_min > 0) and (self._lookback_period > 0)
|
||||
|
||||
if self._use_range & (self._refresh_period < _tf_in_sec):
|
||||
if self._use_range and (self._refresh_period < _tf_in_sec):
|
||||
raise OperationalException(
|
||||
f"Refresh period of {self._refresh_period} seconds is smaller than one "
|
||||
f"timeframe of {self._lookback_timeframe}. Please adjust refresh_period "
|
||||
@@ -78,9 +78,11 @@ class VolumePairList(IPairList):
|
||||
and self._exchange.get_option("tickers_have_quoteVolume")
|
||||
):
|
||||
raise OperationalException(
|
||||
"Exchange does not support dynamic whitelist in this configuration. "
|
||||
"Please edit your config and either remove Volumepairlist, "
|
||||
"or switch to using candles. and restart the bot."
|
||||
f"Exchange {self._exchange.name} does not support dynamic whitelist in this "
|
||||
"configuration. Please edit your config and either remove Volumepairlist, "
|
||||
"or switch to using candles and restart the bot. "
|
||||
f"You can find more information about this in the documentation under "
|
||||
f"{DOCS_LINK}/plugins/#volumepairlist-advanced-mode ."
|
||||
)
|
||||
|
||||
if not self._validate_keys(self._sort_key):
|
||||
@@ -297,7 +299,7 @@ class VolumePairList(IPairList):
|
||||
# Tickers mode - filter based on incoming pairlist.
|
||||
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
|
||||
|
||||
if self._min_value > 0:
|
||||
if self._min_value and self._min_value > 0:
|
||||
filtered_tickers = [v for v in filtered_tickers if v[self._sort_key] > self._min_value]
|
||||
if self._max_value is not None:
|
||||
filtered_tickers = [v for v in filtered_tickers if v[self._sort_key] < self._max_value]
|
||||
|
||||
@@ -51,8 +51,8 @@ class PairListManager(LoggingMixin):
|
||||
invalid = ". ".join([p.name for p in self._pairlist_handlers if p.needstickers])
|
||||
|
||||
raise OperationalException(
|
||||
"Exchange does not support fetchTickers, therefore the following pairlists "
|
||||
"cannot be used. Please edit your config and restart the bot.\n"
|
||||
f"Exchange {self._exchange.name} does not support fetchTickers, therefore the "
|
||||
"following pairlists cannot be used. Please edit your config and restart the bot.\n"
|
||||
f"{invalid}."
|
||||
)
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class IResolver:
|
||||
:return: object class
|
||||
"""
|
||||
logger.debug(f"Searching for {cls.object_type.__name__} {object_name} in '{directory}'")
|
||||
for entry in directory.iterdir():
|
||||
for entry in sorted(directory.iterdir()):
|
||||
# Only consider python files
|
||||
if entry.suffix != ".py":
|
||||
logger.debug("Ignoring %s", entry)
|
||||
@@ -148,7 +148,7 @@ class IResolver:
|
||||
logger.debug("Ignoring broken symlink %s", entry)
|
||||
continue
|
||||
module_path = entry.resolve()
|
||||
if entry.read_text().find(f"class {object_name}(") == -1:
|
||||
if entry.read_text(encoding="utf-8").find(f"class {object_name}(") == -1:
|
||||
logger.debug(f"Skipping {module_path} as it does not contain class {object_name}.")
|
||||
continue
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/background", response_model=list[BackgroundTaskStatus], tags=["webserver"])
|
||||
@router.get("/background", response_model=list[BackgroundTaskStatus])
|
||||
def background_job_list():
|
||||
return [
|
||||
{
|
||||
@@ -29,7 +29,7 @@ def background_job_list():
|
||||
]
|
||||
|
||||
|
||||
@router.get("/background/{jobid}", response_model=BackgroundTaskStatus, tags=["webserver"])
|
||||
@router.get("/background/{jobid}", response_model=BackgroundTaskStatus)
|
||||
def background_job(jobid: str):
|
||||
if not (job := ApiBG.jobs.get(jobid)):
|
||||
raise HTTPException(status_code=404, detail="Job not found.")
|
||||
|
||||
@@ -52,29 +52,25 @@ def __run_backtest_bg(btconfig: Config):
|
||||
lastconfig = ApiBG.bt["last_config"]
|
||||
strat = StrategyResolver.load_strategy(btconfig)
|
||||
validate_config_consistency(btconfig)
|
||||
|
||||
if (
|
||||
not ApiBG.bt["bt"]
|
||||
or lastconfig.get("timeframe") != strat.timeframe
|
||||
time_settings_changed = (
|
||||
lastconfig.get("timeframe") != strat.timeframe
|
||||
or lastconfig.get("timeframe_detail") != btconfig.get("timeframe_detail")
|
||||
or lastconfig.get("timerange") != btconfig["timerange"]
|
||||
):
|
||||
)
|
||||
|
||||
if not ApiBG.bt["bt"] or time_settings_changed:
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
|
||||
ApiBG.bt["bt"] = Backtesting(btconfig)
|
||||
else:
|
||||
ApiBG.bt["bt"].config = deep_merge_dicts(btconfig, ApiBG.bt["bt"].config)
|
||||
ApiBG.bt["bt"].init_backtest()
|
||||
# Only reload data if timeframe changed.
|
||||
if (
|
||||
not ApiBG.bt["data"]
|
||||
or not ApiBG.bt["timerange"]
|
||||
or lastconfig.get("timeframe") != strat.timeframe
|
||||
or lastconfig.get("timerange") != btconfig["timerange"]
|
||||
):
|
||||
# Only reload data if timerange is open or settings changed
|
||||
if not ApiBG.bt["data"] or not ApiBG.bt["timerange"] or time_settings_changed:
|
||||
ApiBG.bt["data"], ApiBG.bt["timerange"] = ApiBG.bt["bt"].load_bt_data()
|
||||
|
||||
lastconfig["timerange"] = btconfig["timerange"]
|
||||
lastconfig["timeframe_detail"] = btconfig.get("timeframe_detail")
|
||||
lastconfig["timeframe"] = strat.timeframe
|
||||
lastconfig["enable_protections"] = btconfig.get("enable_protections")
|
||||
lastconfig["dry_run_wallet"] = btconfig.get("dry_run_wallet")
|
||||
@@ -129,7 +125,7 @@ def __run_backtest_bg(btconfig: Config):
|
||||
ApiBG.bgtask_running = False
|
||||
|
||||
|
||||
@router.post("/backtest", response_model=BacktestResponse, tags=["webserver", "backtest"])
|
||||
@router.post("/backtest", response_model=BacktestResponse)
|
||||
async def api_start_backtest(
|
||||
bt_settings: BacktestRequest, background_tasks: BackgroundTasks, config=Depends(get_config)
|
||||
):
|
||||
@@ -172,7 +168,7 @@ async def api_start_backtest(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/backtest", response_model=BacktestResponse, tags=["webserver", "backtest"])
|
||||
@router.get("/backtest", response_model=BacktestResponse)
|
||||
def api_get_backtest():
|
||||
"""
|
||||
Get backtesting result.
|
||||
@@ -219,7 +215,7 @@ def api_get_backtest():
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/backtest", response_model=BacktestResponse, tags=["webserver", "backtest"])
|
||||
@router.delete("/backtest", response_model=BacktestResponse)
|
||||
def api_delete_backtest():
|
||||
"""Reset backtesting"""
|
||||
if ApiBG.bgtask_running:
|
||||
@@ -246,7 +242,7 @@ def api_delete_backtest():
|
||||
}
|
||||
|
||||
|
||||
@router.get("/backtest/abort", response_model=BacktestResponse, tags=["webserver", "backtest"])
|
||||
@router.get("/backtest/abort", response_model=BacktestResponse)
|
||||
def api_backtest_abort():
|
||||
if not ApiBG.bgtask_running:
|
||||
return {
|
||||
@@ -266,17 +262,13 @@ def api_backtest_abort():
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/backtest/history", response_model=list[BacktestHistoryEntry], tags=["webserver", "backtest"]
|
||||
)
|
||||
@router.get("/backtest/history", response_model=list[BacktestHistoryEntry])
|
||||
def api_backtest_history(config=Depends(get_config)):
|
||||
# Get backtest result history, read from metadata files
|
||||
return get_backtest_resultlist(config["user_data_dir"] / "backtest_results")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/backtest/history/result", response_model=BacktestResponse, tags=["webserver", "backtest"]
|
||||
)
|
||||
@router.get("/backtest/history/result", response_model=BacktestResponse)
|
||||
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config)):
|
||||
# Get backtest result history, read from metadata files
|
||||
bt_results_base: Path = config["user_data_dir"] / "backtest_results"
|
||||
@@ -303,11 +295,7 @@ def api_backtest_history_result(filename: str, strategy: str, config=Depends(get
|
||||
}
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/backtest/history/{file}",
|
||||
response_model=list[BacktestHistoryEntry],
|
||||
tags=["webserver", "backtest"],
|
||||
)
|
||||
@router.delete("/backtest/history/{file}", response_model=list[BacktestHistoryEntry])
|
||||
def api_delete_backtest_history_entry(file: str, config=Depends(get_config)):
|
||||
# Get backtest result history, read from metadata files
|
||||
bt_results_base: Path = config["user_data_dir"] / "backtest_results"
|
||||
@@ -323,11 +311,7 @@ def api_delete_backtest_history_entry(file: str, config=Depends(get_config)):
|
||||
return get_backtest_resultlist(config["user_data_dir"] / "backtest_results")
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/backtest/history/{file}",
|
||||
response_model=list[BacktestHistoryEntry],
|
||||
tags=["webserver", "backtest"],
|
||||
)
|
||||
@router.patch("/backtest/history/{file}", response_model=list[BacktestHistoryEntry])
|
||||
def api_update_backtest_history_entry(
|
||||
file: str, body: BacktestMetadataUpdate, config=Depends(get_config)
|
||||
):
|
||||
@@ -350,11 +334,7 @@ def api_update_backtest_history_entry(
|
||||
return get_backtest_result(file_abs)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/backtest/history/{file}/market_change",
|
||||
response_model=BacktestMarketChange,
|
||||
tags=["webserver", "backtest"],
|
||||
)
|
||||
@router.get("/backtest/history/{file}/market_change", response_model=BacktestMarketChange)
|
||||
def api_get_backtest_market_change(file: str, config=Depends(get_config)):
|
||||
bt_results_base: Path = config["user_data_dir"] / "backtest_results"
|
||||
for fn in (
|
||||
|
||||
@@ -17,7 +17,7 @@ from freqtrade.util.progress_tracker import get_progress_tracker
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Private API, protected by authentication and webserver_mode dependency
|
||||
router = APIRouter(tags=["download-data", "webserver"])
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def __run_download(job_id: str, config_loc: Config):
|
||||
@@ -63,6 +63,8 @@ def pairlists_evaluate(
|
||||
config_loc["timeframes"] = payload.timeframes
|
||||
config_loc["erase"] = payload.erase
|
||||
config_loc["download_trades"] = payload.download_trades
|
||||
if payload.candle_types is not None:
|
||||
config_loc["candle_types"] = payload.candle_types
|
||||
|
||||
handleExchangePayload(payload, config_loc)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/pair_history", response_model=PairHistory, tags=["candle data"])
|
||||
@router.get("/pair_history", response_model=PairHistory, tags=["Candle data"])
|
||||
def pair_history(
|
||||
pair: str,
|
||||
timeframe: str,
|
||||
@@ -43,7 +43,7 @@ def pair_history(
|
||||
raise HTTPException(status_code=502, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/pair_history", response_model=PairHistory, tags=["candle data"])
|
||||
@router.post("/pair_history", response_model=PairHistory, tags=["Candle data"])
|
||||
def pair_history_filtered(payload: PairHistoryRequest, config=Depends(get_config)):
|
||||
# The initial call to this endpoint can be slow, as it may need to initialize
|
||||
# the exchange class.
|
||||
|
||||
@@ -25,9 +25,7 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/pairlists/available", response_model=PairListsResponse, tags=["pairlists", "webserver"]
|
||||
)
|
||||
@router.get("/pairlists/available", response_model=PairListsResponse)
|
||||
def list_pairlists(config=Depends(get_config)):
|
||||
from freqtrade.resolvers import PairListResolver
|
||||
|
||||
@@ -72,7 +70,7 @@ def __run_pairlist(job_id: str, config_loc: Config):
|
||||
ApiBG.pairlist_running = False
|
||||
|
||||
|
||||
@router.post("/pairlists/evaluate", response_model=BgJobStarted, tags=["pairlists", "webserver"])
|
||||
@router.post("/pairlists/evaluate", response_model=BgJobStarted)
|
||||
def pairlists_evaluate(
|
||||
payload: PairListsPayload, background_tasks: BackgroundTasks, config=Depends(get_config)
|
||||
):
|
||||
@@ -126,11 +124,7 @@ def handleExchangePayload(payload: ExchangeModePayloadMixin, config_loc: Config)
|
||||
config_loc["margin_mode"] = payload.margin_mode
|
||||
|
||||
|
||||
@router.get(
|
||||
"/pairlists/evaluate/{jobid}",
|
||||
response_model=WhitelistEvaluateResponse,
|
||||
tags=["pairlists", "webserver"],
|
||||
)
|
||||
@router.get("/pairlists/evaluate/{jobid}", response_model=WhitelistEvaluateResponse)
|
||||
def pairlists_evaluate_get(jobid: str):
|
||||
if not (job := ApiBG.jobs.get(jobid)):
|
||||
raise HTTPException(status_code=404, detail="Job not found.")
|
||||
|
||||
@@ -157,6 +157,11 @@ class Profit(BaseModel):
|
||||
winrate: float
|
||||
expectancy: float
|
||||
expectancy_ratio: float
|
||||
sharpe: float
|
||||
sortino: float
|
||||
sqn: float
|
||||
calmar: float
|
||||
cagr: float
|
||||
max_drawdown: float
|
||||
max_drawdown_abs: float
|
||||
max_drawdown_start: str
|
||||
@@ -426,6 +431,7 @@ class ForceExitPayload(BaseModel):
|
||||
tradeid: str | int
|
||||
ordertype: OrderTypeValues | None = None
|
||||
amount: float | None = None
|
||||
price: float | None = None
|
||||
|
||||
|
||||
class BlacklistPayload(BaseModel):
|
||||
@@ -506,6 +512,7 @@ class DownloadDataPayload(ExchangeModePayloadMixin, BaseModel):
|
||||
timerange: str | None = None
|
||||
erase: bool = False
|
||||
download_trades: bool = False
|
||||
candle_types: list[str] | None = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
def check_mutually_exclusive(cls, values):
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.exceptions import HTTPException
|
||||
|
||||
from freqtrade.enums import TradingMode
|
||||
from freqtrade.rpc import RPC
|
||||
from freqtrade.rpc.api_server.api_schemas import (
|
||||
Balances,
|
||||
BlacklistPayload,
|
||||
BlacklistResponse,
|
||||
Count,
|
||||
DailyWeeklyMonthly,
|
||||
DeleteLockRequest,
|
||||
DeleteTrade,
|
||||
Entry,
|
||||
Exit,
|
||||
ForceEnterPayload,
|
||||
ForceEnterResponse,
|
||||
ForceExitPayload,
|
||||
ListCustomData,
|
||||
Locks,
|
||||
LocksPayload,
|
||||
MixTag,
|
||||
OpenTradeSchema,
|
||||
PairCandlesRequest,
|
||||
PairHistory,
|
||||
PerformanceEntry,
|
||||
Profit,
|
||||
ProfitAll,
|
||||
ResultMsg,
|
||||
Stats,
|
||||
StatusMsg,
|
||||
WhitelistResponse,
|
||||
)
|
||||
from freqtrade.rpc.api_server.deps import get_config, get_rpc
|
||||
from freqtrade.rpc.rpc import RPCException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/balance", response_model=Balances, tags=["Trading-info"])
|
||||
def balance(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
"""Account Balances"""
|
||||
return rpc._rpc_balance(
|
||||
config["stake_currency"],
|
||||
config.get("fiat_display_currency", ""),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/count", response_model=Count, tags=["Trading-info"])
|
||||
def count(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_count()
|
||||
|
||||
|
||||
@router.get("/entries", response_model=list[Entry], tags=["Trading-info"])
|
||||
def entries(pair: str | None = None, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_enter_tag_performance(pair)
|
||||
|
||||
|
||||
@router.get("/exits", response_model=list[Exit], tags=["Trading-info"])
|
||||
def exits(pair: str | None = None, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_exit_reason_performance(pair)
|
||||
|
||||
|
||||
@router.get("/mix_tags", response_model=list[MixTag], tags=["Trading-info"])
|
||||
def mix_tags(pair: str | None = None, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_mix_tag_performance(pair)
|
||||
|
||||
|
||||
@router.get("/performance", response_model=list[PerformanceEntry], tags=["Trading-info"])
|
||||
def performance(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_performance()
|
||||
|
||||
|
||||
@router.get("/profit", response_model=Profit, tags=["Trading-info"])
|
||||
def profit(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
return rpc._rpc_trade_statistics(config["stake_currency"], config.get("fiat_display_currency"))
|
||||
|
||||
|
||||
@router.get("/profit_all", response_model=ProfitAll, tags=["Trading-info"])
|
||||
def profit_all(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
response = {
|
||||
"all": rpc._rpc_trade_statistics(
|
||||
config["stake_currency"], config.get("fiat_display_currency")
|
||||
),
|
||||
}
|
||||
if config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
|
||||
response["long"] = rpc._rpc_trade_statistics(
|
||||
config["stake_currency"], config.get("fiat_display_currency"), direction="long"
|
||||
)
|
||||
response["short"] = rpc._rpc_trade_statistics(
|
||||
config["stake_currency"], config.get("fiat_display_currency"), direction="short"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/stats", response_model=Stats, tags=["Trading-info"])
|
||||
def stats(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_stats()
|
||||
|
||||
|
||||
@router.get("/daily", response_model=DailyWeeklyMonthly, tags=["Trading-info"])
|
||||
def daily(
|
||||
timescale: int = Query(7, ge=1, description="Number of days to fetch data for"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
config=Depends(get_config),
|
||||
):
|
||||
return rpc._rpc_timeunit_profit(
|
||||
timescale, config["stake_currency"], config.get("fiat_display_currency", "")
|
||||
)
|
||||
|
||||
|
||||
@router.get("/weekly", response_model=DailyWeeklyMonthly, tags=["Trading-info"])
|
||||
def weekly(
|
||||
timescale: int = Query(4, ge=1, description="Number of weeks to fetch data for"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
config=Depends(get_config),
|
||||
):
|
||||
return rpc._rpc_timeunit_profit(
|
||||
timescale, config["stake_currency"], config.get("fiat_display_currency", ""), "weeks"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/monthly", response_model=DailyWeeklyMonthly, tags=["Trading-info"])
|
||||
def monthly(
|
||||
timescale: int = Query(3, ge=1, description="Number of months to fetch data for"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
config=Depends(get_config),
|
||||
):
|
||||
return rpc._rpc_timeunit_profit(
|
||||
timescale, config["stake_currency"], config.get("fiat_display_currency", ""), "months"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/status", response_model=list[OpenTradeSchema], tags=["Trading-info"])
|
||||
def status(rpc: RPC = Depends(get_rpc)):
|
||||
try:
|
||||
return rpc._rpc_trade_status()
|
||||
except RPCException:
|
||||
return []
|
||||
|
||||
|
||||
# Using the responsemodel here will cause a ~100% increase in response time (from 1s to 2s)
|
||||
# on big databases. Correct response model: response_model=TradeResponse,
|
||||
@router.get("/trades", tags=["Trading-info", "Trades"])
|
||||
def trades(
|
||||
limit: int = Query(500, ge=1, description="Maximum number of different trades to return data"),
|
||||
offset: int = Query(0, ge=0, description="Number of trades to skip for pagination"),
|
||||
order_by_id: bool = Query(
|
||||
True, description="Sort trades by id (default: True). If False, sorts by latest timestamp"
|
||||
),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
):
|
||||
return rpc._rpc_trade_history(limit, offset=offset, order_by_id=order_by_id)
|
||||
|
||||
|
||||
@router.get("/trade/{tradeid}", response_model=OpenTradeSchema, tags=["Trades"])
|
||||
def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)):
|
||||
try:
|
||||
return rpc._rpc_trade_status([tradeid])[0]
|
||||
except (RPCException, KeyError):
|
||||
raise HTTPException(status_code=404, detail="Trade not found.")
|
||||
|
||||
|
||||
@router.delete("/trades/{tradeid}", response_model=DeleteTrade, tags=["Trades"])
|
||||
def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_delete(tradeid)
|
||||
|
||||
|
||||
@router.delete("/trades/{tradeid}/open-order", response_model=OpenTradeSchema, tags=["Trades"])
|
||||
def trade_cancel_open_order(tradeid: int, rpc: RPC = Depends(get_rpc)):
|
||||
rpc._rpc_cancel_open_order(tradeid)
|
||||
return rpc._rpc_trade_status([tradeid])[0]
|
||||
|
||||
|
||||
@router.post("/trades/{tradeid}/reload", response_model=OpenTradeSchema, tags=["Trades"])
|
||||
def trade_reload(tradeid: int, rpc: RPC = Depends(get_rpc)):
|
||||
rpc._rpc_reload_trade_from_exchange(tradeid)
|
||||
return rpc._rpc_trade_status([tradeid])[0]
|
||||
|
||||
|
||||
@router.get("/trades/open/custom-data", response_model=list[ListCustomData], tags=["Trades"])
|
||||
def list_open_trades_custom_data(
|
||||
key: str | None = Query(None, description="Optional key to filter data"),
|
||||
limit: int = Query(100, ge=1, description="Maximum number of different trades to return data"),
|
||||
offset: int = Query(0, ge=0, description="Number of trades to skip for pagination"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
):
|
||||
"""
|
||||
Fetch custom data for all open trades.
|
||||
If a key is provided, it will be used to filter data accordingly.
|
||||
Pagination is implemented via the `limit` and `offset` parameters.
|
||||
"""
|
||||
try:
|
||||
return rpc._rpc_list_custom_data(key=key, limit=limit, offset=offset)
|
||||
except RPCException as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/trades/{trade_id}/custom-data", response_model=list[ListCustomData], tags=["Trades"])
|
||||
def list_custom_data(trade_id: int, key: str | None = Query(None), rpc: RPC = Depends(get_rpc)):
|
||||
"""
|
||||
Fetch custom data for a specific trade.
|
||||
If a key is provided, it will be used to filter data accordingly.
|
||||
"""
|
||||
try:
|
||||
return rpc._rpc_list_custom_data(trade_id, key=key)
|
||||
except RPCException as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
# /forcebuy is deprecated with short addition. use /forceentry instead
|
||||
@router.post("/forceenter", response_model=ForceEnterResponse, tags=["Trades"])
|
||||
@router.post(
|
||||
"/forcebuy",
|
||||
response_model=ForceEnterResponse,
|
||||
tags=["Trades"],
|
||||
summary="(deprecated) Please use /forceenter instead",
|
||||
)
|
||||
def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
|
||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||
|
||||
trade = rpc._rpc_force_entry(
|
||||
payload.pair,
|
||||
payload.price,
|
||||
order_side=payload.side,
|
||||
order_type=ordertype,
|
||||
stake_amount=payload.stakeamount,
|
||||
enter_tag=payload.entry_tag or "force_entry",
|
||||
leverage=payload.leverage,
|
||||
)
|
||||
|
||||
if trade:
|
||||
return ForceEnterResponse.model_validate(trade.to_json())
|
||||
else:
|
||||
return ForceEnterResponse.model_validate(
|
||||
{"status": f"Error entering {payload.side} trade for pair {payload.pair}."}
|
||||
)
|
||||
|
||||
|
||||
# /forcesell is deprecated with short addition. use /forceexit instead
|
||||
@router.post("/forceexit", response_model=ResultMsg, tags=["Trades"])
|
||||
@router.post(
|
||||
"/forcesell",
|
||||
response_model=ResultMsg,
|
||||
tags=["Trades"],
|
||||
summary="(deprecated) Please use /forceexit instead",
|
||||
)
|
||||
def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
|
||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||
return rpc._rpc_force_exit(
|
||||
str(payload.tradeid), ordertype, amount=payload.amount, price=payload.price
|
||||
)
|
||||
|
||||
|
||||
@router.get("/blacklist", response_model=BlacklistResponse, tags=["Trading-info", "Pairlist"])
|
||||
def blacklist(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_blacklist()
|
||||
|
||||
|
||||
@router.post("/blacklist", response_model=BlacklistResponse, tags=["Pairlist"])
|
||||
def blacklist_post(payload: BlacklistPayload, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_blacklist(payload.blacklist)
|
||||
|
||||
|
||||
@router.delete("/blacklist", response_model=BlacklistResponse, tags=["Pairlist"])
|
||||
def blacklist_delete(pairs_to_delete: list[str] = Query([]), rpc: RPC = Depends(get_rpc)):
|
||||
"""Provide a list of pairs to delete from the blacklist"""
|
||||
|
||||
return rpc._rpc_blacklist_delete(pairs_to_delete)
|
||||
|
||||
|
||||
@router.get("/whitelist", response_model=WhitelistResponse, tags=["Trading-info", "Pairlist"])
|
||||
def whitelist(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_whitelist()
|
||||
|
||||
|
||||
@router.get("/locks", response_model=Locks, tags=["Trading-info", "Locks"])
|
||||
def locks(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_locks()
|
||||
|
||||
|
||||
@router.delete("/locks/{lockid}", response_model=Locks, tags=["Locks"])
|
||||
def delete_lock(lockid: int, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_delete_lock(lockid=lockid)
|
||||
|
||||
|
||||
@router.post("/locks/delete", response_model=Locks, tags=["Locks"])
|
||||
def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_delete_lock(lockid=payload.lockid, pair=payload.pair)
|
||||
|
||||
|
||||
@router.post("/locks", response_model=Locks, tags=["Locks"])
|
||||
def add_locks(payload: list[LocksPayload], rpc: RPC = Depends(get_rpc)):
|
||||
for lock in payload:
|
||||
rpc._rpc_add_lock(lock.pair, lock.until, lock.reason, lock.side)
|
||||
return rpc._rpc_locks()
|
||||
|
||||
|
||||
@router.post("/start", response_model=StatusMsg, tags=["Bot-control"])
|
||||
def start(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_start()
|
||||
|
||||
|
||||
@router.post("/stop", response_model=StatusMsg, tags=["Bot-control"])
|
||||
def stop(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_stop()
|
||||
|
||||
|
||||
@router.post("/pause", response_model=StatusMsg, tags=["Bot-control"])
|
||||
@router.post("/stopentry", response_model=StatusMsg, tags=["Bot-control"])
|
||||
@router.post("/stopbuy", response_model=StatusMsg, tags=["Bot-control"])
|
||||
def pause(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_pause()
|
||||
|
||||
|
||||
@router.post("/reload_config", response_model=StatusMsg, tags=["Bot-control"])
|
||||
def reload_config(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_reload_config()
|
||||
|
||||
|
||||
@router.get("/pair_candles", response_model=PairHistory, tags=["Candle data"])
|
||||
def pair_candles(pair: str, timeframe: str, limit: int | None = None, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_analysed_dataframe(pair, timeframe, limit, None)
|
||||
|
||||
|
||||
@router.post("/pair_candles", response_model=PairHistory, tags=["Candle data"])
|
||||
def pair_candles_filtered(payload: PairCandlesRequest, rpc: RPC = Depends(get_rpc)):
|
||||
# Advanced pair_candles endpoint with column filtering
|
||||
return rpc._rpc_analysed_dataframe(
|
||||
payload.pair, payload.timeframe, payload.limit, payload.columns
|
||||
)
|
||||
@@ -6,53 +6,19 @@ from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.exceptions import HTTPException
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.data.history import get_datahandler
|
||||
from freqtrade.enums import CandleType, RunMode, State, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.enums import RunMode, State
|
||||
from freqtrade.rpc import RPC
|
||||
from freqtrade.rpc.api_server.api_pairlists import handleExchangePayload
|
||||
from freqtrade.rpc.api_server.api_schemas import (
|
||||
AvailablePairs,
|
||||
Balances,
|
||||
BlacklistPayload,
|
||||
BlacklistResponse,
|
||||
Count,
|
||||
DailyWeeklyMonthly,
|
||||
DeleteLockRequest,
|
||||
DeleteTrade,
|
||||
Entry,
|
||||
ExchangeListResponse,
|
||||
Exit,
|
||||
ForceEnterPayload,
|
||||
ForceEnterResponse,
|
||||
ForceExitPayload,
|
||||
FreqAIModelListResponse,
|
||||
Health,
|
||||
HyperoptLossListResponse,
|
||||
ListCustomData,
|
||||
Locks,
|
||||
LocksPayload,
|
||||
Logs,
|
||||
MarketRequest,
|
||||
MarketResponse,
|
||||
MixTag,
|
||||
OpenTradeSchema,
|
||||
PairCandlesRequest,
|
||||
PairHistory,
|
||||
PerformanceEntry,
|
||||
Ping,
|
||||
PlotConfig,
|
||||
Profit,
|
||||
ProfitAll,
|
||||
ResultMsg,
|
||||
ShowConfig,
|
||||
Stats,
|
||||
StatusMsg,
|
||||
StrategyListResponse,
|
||||
StrategyResponse,
|
||||
SysInfo,
|
||||
Version,
|
||||
WhitelistResponse,
|
||||
)
|
||||
from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional
|
||||
from freqtrade.rpc.rpc import RPCException
|
||||
@@ -91,7 +57,9 @@ logger = logging.getLogger(__name__)
|
||||
# 2.41: Add download-data endpoint
|
||||
# 2.42: Add /pair_history endpoint with live data
|
||||
# 2.43: Add /profit_all endpoint
|
||||
API_VERSION = 2.43
|
||||
# 2.44: Add candle_types parameter to download-data endpoint
|
||||
# 2.45: Add price to forceexit endpoint
|
||||
API_VERSION = 2.45
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
@@ -99,191 +67,19 @@ router_public = APIRouter()
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router_public.get("/ping", response_model=Ping)
|
||||
@router_public.get("/ping", response_model=Ping, tags=["Info"])
|
||||
def ping():
|
||||
"""simple ping"""
|
||||
return {"status": "pong"}
|
||||
|
||||
|
||||
@router.get("/version", response_model=Version, tags=["info"])
|
||||
@router.get("/version", response_model=Version, tags=["Info"])
|
||||
def version():
|
||||
"""Bot Version info"""
|
||||
return {"version": __version__}
|
||||
|
||||
|
||||
@router.get("/balance", response_model=Balances, tags=["info"])
|
||||
def balance(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
"""Account Balances"""
|
||||
return rpc._rpc_balance(
|
||||
config["stake_currency"],
|
||||
config.get("fiat_display_currency", ""),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/count", response_model=Count, tags=["info"])
|
||||
def count(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_count()
|
||||
|
||||
|
||||
@router.get("/entries", response_model=list[Entry], tags=["info"])
|
||||
def entries(pair: str | None = None, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_enter_tag_performance(pair)
|
||||
|
||||
|
||||
@router.get("/exits", response_model=list[Exit], tags=["info"])
|
||||
def exits(pair: str | None = None, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_exit_reason_performance(pair)
|
||||
|
||||
|
||||
@router.get("/mix_tags", response_model=list[MixTag], tags=["info"])
|
||||
def mix_tags(pair: str | None = None, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_mix_tag_performance(pair)
|
||||
|
||||
|
||||
@router.get("/performance", response_model=list[PerformanceEntry], tags=["info"])
|
||||
def performance(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_performance()
|
||||
|
||||
|
||||
@router.get("/profit", response_model=Profit, tags=["info"])
|
||||
def profit(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
return rpc._rpc_trade_statistics(config["stake_currency"], config.get("fiat_display_currency"))
|
||||
|
||||
|
||||
@router.get("/profit_all", response_model=ProfitAll, tags=["info"])
|
||||
def profit_all(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
response = {
|
||||
"all": rpc._rpc_trade_statistics(
|
||||
config["stake_currency"], config.get("fiat_display_currency")
|
||||
),
|
||||
}
|
||||
if config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
|
||||
response["long"] = rpc._rpc_trade_statistics(
|
||||
config["stake_currency"], config.get("fiat_display_currency"), direction="long"
|
||||
)
|
||||
response["short"] = rpc._rpc_trade_statistics(
|
||||
config["stake_currency"], config.get("fiat_display_currency"), direction="short"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/stats", response_model=Stats, tags=["info"])
|
||||
def stats(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_stats()
|
||||
|
||||
|
||||
@router.get("/daily", response_model=DailyWeeklyMonthly, tags=["info"])
|
||||
def daily(
|
||||
timescale: int = Query(7, ge=1, description="Number of days to fetch data for"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
config=Depends(get_config),
|
||||
):
|
||||
return rpc._rpc_timeunit_profit(
|
||||
timescale, config["stake_currency"], config.get("fiat_display_currency", "")
|
||||
)
|
||||
|
||||
|
||||
@router.get("/weekly", response_model=DailyWeeklyMonthly, tags=["info"])
|
||||
def weekly(
|
||||
timescale: int = Query(4, ge=1, description="Number of weeks to fetch data for"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
config=Depends(get_config),
|
||||
):
|
||||
return rpc._rpc_timeunit_profit(
|
||||
timescale, config["stake_currency"], config.get("fiat_display_currency", ""), "weeks"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/monthly", response_model=DailyWeeklyMonthly, tags=["info"])
|
||||
def monthly(
|
||||
timescale: int = Query(3, ge=1, description="Number of months to fetch data for"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
config=Depends(get_config),
|
||||
):
|
||||
return rpc._rpc_timeunit_profit(
|
||||
timescale, config["stake_currency"], config.get("fiat_display_currency", ""), "months"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/status", response_model=list[OpenTradeSchema], tags=["info"])
|
||||
def status(rpc: RPC = Depends(get_rpc)):
|
||||
try:
|
||||
return rpc._rpc_trade_status()
|
||||
except RPCException:
|
||||
return []
|
||||
|
||||
|
||||
# Using the responsemodel here will cause a ~100% increase in response time (from 1s to 2s)
|
||||
# on big databases. Correct response model: response_model=TradeResponse,
|
||||
@router.get("/trades", tags=["info", "trading"])
|
||||
def trades(
|
||||
limit: int = Query(500, ge=1, description="Maximum number of different trades to return data"),
|
||||
offset: int = Query(0, ge=0, description="Number of trades to skip for pagination"),
|
||||
order_by_id: bool = Query(
|
||||
True, description="Sort trades by id (default: True). If False, sorts by latest timestamp"
|
||||
),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
):
|
||||
return rpc._rpc_trade_history(limit, offset=offset, order_by_id=order_by_id)
|
||||
|
||||
|
||||
@router.get("/trade/{tradeid}", response_model=OpenTradeSchema, tags=["info", "trading"])
|
||||
def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)):
|
||||
try:
|
||||
return rpc._rpc_trade_status([tradeid])[0]
|
||||
except (RPCException, KeyError):
|
||||
raise HTTPException(status_code=404, detail="Trade not found.")
|
||||
|
||||
|
||||
@router.delete("/trades/{tradeid}", response_model=DeleteTrade, tags=["info", "trading"])
|
||||
def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_delete(tradeid)
|
||||
|
||||
|
||||
@router.delete("/trades/{tradeid}/open-order", response_model=OpenTradeSchema, tags=["trading"])
|
||||
def trade_cancel_open_order(tradeid: int, rpc: RPC = Depends(get_rpc)):
|
||||
rpc._rpc_cancel_open_order(tradeid)
|
||||
return rpc._rpc_trade_status([tradeid])[0]
|
||||
|
||||
|
||||
@router.post("/trades/{tradeid}/reload", response_model=OpenTradeSchema, tags=["trading"])
|
||||
def trade_reload(tradeid: int, rpc: RPC = Depends(get_rpc)):
|
||||
rpc._rpc_reload_trade_from_exchange(tradeid)
|
||||
return rpc._rpc_trade_status([tradeid])[0]
|
||||
|
||||
|
||||
@router.get("/trades/open/custom-data", response_model=list[ListCustomData], tags=["trading"])
|
||||
def list_open_trades_custom_data(
|
||||
key: str | None = Query(None, description="Optional key to filter data"),
|
||||
limit: int = Query(100, ge=1, description="Maximum number of different trades to return data"),
|
||||
offset: int = Query(0, ge=0, description="Number of trades to skip for pagination"),
|
||||
rpc: RPC = Depends(get_rpc),
|
||||
):
|
||||
"""
|
||||
Fetch custom data for all open trades.
|
||||
If a key is provided, it will be used to filter data accordingly.
|
||||
Pagination is implemented via the `limit` and `offset` parameters.
|
||||
"""
|
||||
try:
|
||||
return rpc._rpc_list_custom_data(key=key, limit=limit, offset=offset)
|
||||
except RPCException as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/trades/{trade_id}/custom-data", response_model=list[ListCustomData], tags=["trading"])
|
||||
def list_custom_data(trade_id: int, key: str | None = Query(None), rpc: RPC = Depends(get_rpc)):
|
||||
"""
|
||||
Fetch custom data for a specific trade.
|
||||
If a key is provided, it will be used to filter data accordingly.
|
||||
"""
|
||||
try:
|
||||
return rpc._rpc_list_custom_data(trade_id, key=key)
|
||||
except RPCException as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/show_config", response_model=ShowConfig, tags=["info"])
|
||||
@router.get("/show_config", response_model=ShowConfig, tags=["Info"])
|
||||
def show_config(rpc: RPC | None = Depends(get_rpc_optional), config=Depends(get_config)):
|
||||
state: State | str = ""
|
||||
strategy_version = None
|
||||
@@ -295,123 +91,12 @@ def show_config(rpc: RPC | None = Depends(get_rpc_optional), config=Depends(get_
|
||||
return resp
|
||||
|
||||
|
||||
# /forcebuy is deprecated with short addition. use /forceentry instead
|
||||
@router.post("/forceenter", response_model=ForceEnterResponse, tags=["trading"])
|
||||
@router.post("/forcebuy", response_model=ForceEnterResponse, tags=["trading"])
|
||||
def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
|
||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||
|
||||
trade = rpc._rpc_force_entry(
|
||||
payload.pair,
|
||||
payload.price,
|
||||
order_side=payload.side,
|
||||
order_type=ordertype,
|
||||
stake_amount=payload.stakeamount,
|
||||
enter_tag=payload.entry_tag or "force_entry",
|
||||
leverage=payload.leverage,
|
||||
)
|
||||
|
||||
if trade:
|
||||
return ForceEnterResponse.model_validate(trade.to_json())
|
||||
else:
|
||||
return ForceEnterResponse.model_validate(
|
||||
{"status": f"Error entering {payload.side} trade for pair {payload.pair}."}
|
||||
)
|
||||
|
||||
|
||||
# /forcesell is deprecated with short addition. use /forceexit instead
|
||||
@router.post("/forceexit", response_model=ResultMsg, tags=["trading"])
|
||||
@router.post("/forcesell", response_model=ResultMsg, tags=["trading"])
|
||||
def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
|
||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||
return rpc._rpc_force_exit(str(payload.tradeid), ordertype, amount=payload.amount)
|
||||
|
||||
|
||||
@router.get("/blacklist", response_model=BlacklistResponse, tags=["info", "pairlist"])
|
||||
def blacklist(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_blacklist()
|
||||
|
||||
|
||||
@router.post("/blacklist", response_model=BlacklistResponse, tags=["info", "pairlist"])
|
||||
def blacklist_post(payload: BlacklistPayload, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_blacklist(payload.blacklist)
|
||||
|
||||
|
||||
@router.delete("/blacklist", response_model=BlacklistResponse, tags=["info", "pairlist"])
|
||||
def blacklist_delete(pairs_to_delete: list[str] = Query([]), rpc: RPC = Depends(get_rpc)):
|
||||
"""Provide a list of pairs to delete from the blacklist"""
|
||||
|
||||
return rpc._rpc_blacklist_delete(pairs_to_delete)
|
||||
|
||||
|
||||
@router.get("/whitelist", response_model=WhitelistResponse, tags=["info", "pairlist"])
|
||||
def whitelist(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_whitelist()
|
||||
|
||||
|
||||
@router.get("/locks", response_model=Locks, tags=["info", "locks"])
|
||||
def locks(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_locks()
|
||||
|
||||
|
||||
@router.delete("/locks/{lockid}", response_model=Locks, tags=["info", "locks"])
|
||||
def delete_lock(lockid: int, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_delete_lock(lockid=lockid)
|
||||
|
||||
|
||||
@router.post("/locks/delete", response_model=Locks, tags=["info", "locks"])
|
||||
def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_delete_lock(lockid=payload.lockid, pair=payload.pair)
|
||||
|
||||
|
||||
@router.post("/locks", response_model=Locks, tags=["info", "locks"])
|
||||
def add_locks(payload: list[LocksPayload], rpc: RPC = Depends(get_rpc)):
|
||||
for lock in payload:
|
||||
rpc._rpc_add_lock(lock.pair, lock.until, lock.reason, lock.side)
|
||||
return rpc._rpc_locks()
|
||||
|
||||
|
||||
@router.get("/logs", response_model=Logs, tags=["info"])
|
||||
@router.get("/logs", response_model=Logs, tags=["Info"])
|
||||
def logs(limit: int | None = None):
|
||||
return RPC._rpc_get_logs(limit)
|
||||
|
||||
|
||||
@router.post("/start", response_model=StatusMsg, tags=["botcontrol"])
|
||||
def start(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_start()
|
||||
|
||||
|
||||
@router.post("/stop", response_model=StatusMsg, tags=["botcontrol"])
|
||||
def stop(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_stop()
|
||||
|
||||
|
||||
@router.post("/pause", response_model=StatusMsg, tags=["botcontrol"])
|
||||
@router.post("/stopentry", response_model=StatusMsg, tags=["botcontrol"])
|
||||
@router.post("/stopbuy", response_model=StatusMsg, tags=["botcontrol"])
|
||||
def pause(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_pause()
|
||||
|
||||
|
||||
@router.post("/reload_config", response_model=StatusMsg, tags=["botcontrol"])
|
||||
def reload_config(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_reload_config()
|
||||
|
||||
|
||||
@router.get("/pair_candles", response_model=PairHistory, tags=["candle data"])
|
||||
def pair_candles(pair: str, timeframe: str, limit: int | None = None, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_analysed_dataframe(pair, timeframe, limit, None)
|
||||
|
||||
|
||||
@router.post("/pair_candles", response_model=PairHistory, tags=["candle data"])
|
||||
def pair_candles_filtered(payload: PairCandlesRequest, rpc: RPC = Depends(get_rpc)):
|
||||
# Advanced pair_candles endpoint with column filtering
|
||||
return rpc._rpc_analysed_dataframe(
|
||||
payload.pair, payload.timeframe, payload.limit, payload.columns
|
||||
)
|
||||
|
||||
|
||||
@router.get("/plot_config", response_model=PlotConfig, tags=["candle data"])
|
||||
@router.get("/plot_config", response_model=PlotConfig, tags=["Candle data"])
|
||||
def plot_config(
|
||||
strategy: str | None = None,
|
||||
config=Depends(get_config),
|
||||
@@ -430,119 +115,7 @@ def plot_config(
|
||||
raise HTTPException(status_code=502, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/strategies", response_model=StrategyListResponse, tags=["strategy"])
|
||||
def list_strategies(config=Depends(get_config)):
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
|
||||
strategies = StrategyResolver.search_all_objects(
|
||||
config, False, config.get("recursive_strategy_search", False)
|
||||
)
|
||||
strategies = sorted(strategies, key=lambda x: x["name"])
|
||||
|
||||
return {"strategies": [x["name"] for x in strategies]}
|
||||
|
||||
|
||||
@router.get("/strategy/{strategy}", response_model=StrategyResponse, tags=["strategy"])
|
||||
def get_strategy(strategy: str, config=Depends(get_config)):
|
||||
if ":" in strategy:
|
||||
raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.")
|
||||
|
||||
config_ = deepcopy(config)
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
|
||||
try:
|
||||
strategy_obj = StrategyResolver._load_strategy(
|
||||
strategy, config_, extra_dir=config_.get("strategy_path")
|
||||
)
|
||||
except OperationalException:
|
||||
raise HTTPException(status_code=404, detail="Strategy not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=str(e))
|
||||
return {
|
||||
"strategy": strategy_obj.get_strategy_name(),
|
||||
"code": strategy_obj.__source__,
|
||||
"timeframe": getattr(strategy_obj, "timeframe", None),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/exchanges", response_model=ExchangeListResponse, tags=[])
|
||||
def list_exchanges(config=Depends(get_config)):
|
||||
from freqtrade.exchange import list_available_exchanges
|
||||
|
||||
exchanges = list_available_exchanges(config)
|
||||
return {
|
||||
"exchanges": exchanges,
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/hyperoptloss", response_model=HyperoptLossListResponse, tags=["hyperopt", "webserver"]
|
||||
)
|
||||
def list_hyperoptloss(
|
||||
config=Depends(get_config),
|
||||
):
|
||||
import textwrap
|
||||
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||
|
||||
loss_functions = HyperOptLossResolver.search_all_objects(config, False)
|
||||
loss_functions = sorted(loss_functions, key=lambda x: x["name"])
|
||||
|
||||
return {
|
||||
"loss_functions": [
|
||||
{
|
||||
"name": x["name"],
|
||||
"description": textwrap.dedent((x["class"].__doc__ or "").strip()),
|
||||
}
|
||||
for x in loss_functions
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/freqaimodels", response_model=FreqAIModelListResponse, tags=["freqai"])
|
||||
def list_freqaimodels(config=Depends(get_config)):
|
||||
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
|
||||
|
||||
models = FreqaiModelResolver.search_all_objects(config, False)
|
||||
models = sorted(models, key=lambda x: x["name"])
|
||||
|
||||
return {"freqaimodels": [x["name"] for x in models]}
|
||||
|
||||
|
||||
@router.get("/available_pairs", response_model=AvailablePairs, tags=["candle data"])
|
||||
def list_available_pairs(
|
||||
timeframe: str | None = None,
|
||||
stake_currency: str | None = None,
|
||||
candletype: CandleType | None = None,
|
||||
config=Depends(get_config),
|
||||
):
|
||||
dh = get_datahandler(config["datadir"], config.get("dataformat_ohlcv"))
|
||||
trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT)
|
||||
pair_interval = dh.ohlcv_get_available_data(config["datadir"], trading_mode)
|
||||
|
||||
if timeframe:
|
||||
pair_interval = [pair for pair in pair_interval if pair[1] == timeframe]
|
||||
if stake_currency:
|
||||
pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)]
|
||||
if candletype:
|
||||
pair_interval = [pair for pair in pair_interval if pair[2] == candletype]
|
||||
else:
|
||||
candle_type = CandleType.get_default(trading_mode)
|
||||
pair_interval = [pair for pair in pair_interval if pair[2] == candle_type]
|
||||
|
||||
pair_interval = sorted(pair_interval, key=lambda x: x[0])
|
||||
|
||||
pairs = list({x[0] for x in pair_interval})
|
||||
pairs.sort()
|
||||
result = {
|
||||
"length": len(pairs),
|
||||
"pairs": pairs,
|
||||
"pair_interval": pair_interval,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/markets", response_model=MarketResponse, tags=["candle data", "webserver"])
|
||||
@router.get("/markets", response_model=MarketResponse, tags=["Candle data"])
|
||||
def markets(
|
||||
query: Annotated[MarketRequest, Query()],
|
||||
config=Depends(get_config),
|
||||
@@ -565,11 +138,11 @@ def markets(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/sysinfo", response_model=SysInfo, tags=["info"])
|
||||
@router.get("/sysinfo", response_model=SysInfo, tags=["Info"])
|
||||
def sysinfo():
|
||||
return RPC._rpc_sysinfo()
|
||||
|
||||
|
||||
@router.get("/health", response_model=Health, tags=["info"])
|
||||
@router.get("/health", response_model=Health, tags=["Info"])
|
||||
def health(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc.health()
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.exceptions import HTTPException
|
||||
|
||||
from freqtrade.data.history.datahandlers import get_datahandler
|
||||
from freqtrade.enums import CandleType, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.rpc.api_server.api_schemas import (
|
||||
AvailablePairs,
|
||||
ExchangeListResponse,
|
||||
FreqAIModelListResponse,
|
||||
HyperoptLossListResponse,
|
||||
StrategyListResponse,
|
||||
StrategyResponse,
|
||||
)
|
||||
from freqtrade.rpc.api_server.deps import get_config
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Private API, protected by authentication and webserver_mode dependency
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/strategies", response_model=StrategyListResponse, tags=["Strategy"])
|
||||
def list_strategies(config=Depends(get_config)):
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
|
||||
strategies = StrategyResolver.search_all_objects(
|
||||
config, False, config.get("recursive_strategy_search", False)
|
||||
)
|
||||
strategies = sorted(strategies, key=lambda x: x["name"])
|
||||
|
||||
return {"strategies": [x["name"] for x in strategies]}
|
||||
|
||||
|
||||
@router.get("/strategy/{strategy}", response_model=StrategyResponse, tags=["Strategy"])
|
||||
def get_strategy(strategy: str, config=Depends(get_config)):
|
||||
if ":" in strategy:
|
||||
raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.")
|
||||
|
||||
config_ = deepcopy(config)
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
|
||||
try:
|
||||
strategy_obj = StrategyResolver._load_strategy(
|
||||
strategy, config_, extra_dir=config_.get("strategy_path")
|
||||
)
|
||||
except OperationalException:
|
||||
raise HTTPException(status_code=404, detail="Strategy not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=str(e))
|
||||
return {
|
||||
"strategy": strategy_obj.get_strategy_name(),
|
||||
"code": strategy_obj.__source__,
|
||||
"timeframe": getattr(strategy_obj, "timeframe", None),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/exchanges", response_model=ExchangeListResponse, tags=[])
|
||||
def list_exchanges(config=Depends(get_config)):
|
||||
from freqtrade.exchange import list_available_exchanges
|
||||
|
||||
exchanges = list_available_exchanges(config)
|
||||
return {
|
||||
"exchanges": exchanges,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/hyperoptloss", response_model=HyperoptLossListResponse, tags=["Hyperopt"])
|
||||
def list_hyperoptloss(
|
||||
config=Depends(get_config),
|
||||
):
|
||||
import textwrap
|
||||
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||
|
||||
loss_functions = HyperOptLossResolver.search_all_objects(config, False)
|
||||
loss_functions = sorted(loss_functions, key=lambda x: x["name"])
|
||||
|
||||
return {
|
||||
"loss_functions": [
|
||||
{
|
||||
"name": x["name"],
|
||||
"description": textwrap.dedent((x["class"].__doc__ or "").strip()),
|
||||
}
|
||||
for x in loss_functions
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/freqaimodels", response_model=FreqAIModelListResponse, tags=["FreqAI"])
|
||||
def list_freqaimodels(config=Depends(get_config)):
|
||||
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
|
||||
|
||||
models = FreqaiModelResolver.search_all_objects(config, False)
|
||||
models = sorted(models, key=lambda x: x["name"])
|
||||
|
||||
return {"freqaimodels": [x["name"] for x in models]}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/available_pairs", response_model=AvailablePairs, tags=["Candle data", "Download-data"]
|
||||
)
|
||||
def list_available_pairs(
|
||||
timeframe: str | None = None,
|
||||
stake_currency: str | None = None,
|
||||
candletype: CandleType | None = None,
|
||||
config=Depends(get_config),
|
||||
):
|
||||
dh = get_datahandler(config["datadir"], config.get("dataformat_ohlcv"))
|
||||
trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT)
|
||||
pair_interval = dh.ohlcv_get_available_data(config["datadir"], trading_mode)
|
||||
|
||||
if timeframe:
|
||||
pair_interval = [pair for pair in pair_interval if pair[1] == timeframe]
|
||||
if stake_currency:
|
||||
pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)]
|
||||
if candletype:
|
||||
pair_interval = [pair for pair in pair_interval if pair[2] == candletype]
|
||||
else:
|
||||
candle_type = CandleType.get_default(trading_mode)
|
||||
pair_interval = [pair for pair in pair_interval if pair[2] == candle_type]
|
||||
|
||||
pair_interval = sorted(pair_interval, key=lambda x: x[0])
|
||||
|
||||
pairs = list({x[0] for x in pair_interval})
|
||||
pairs.sort()
|
||||
result = {
|
||||
"length": len(pairs),
|
||||
"pairs": pairs,
|
||||
"pair_interval": pair_interval,
|
||||
}
|
||||
return result
|
||||
@@ -5,7 +5,7 @@ from uuid import uuid4
|
||||
from fastapi import Depends, HTTPException
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.enums import TRADE_MODES, RunMode
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.persistence.models import _request_id_ctx_var
|
||||
from freqtrade.rpc.api_server.webserver_bgwork import ApiBG
|
||||
@@ -69,3 +69,9 @@ def is_webserver_mode(config=Depends(get_config)):
|
||||
if config["runmode"] != RunMode.WEBSERVER:
|
||||
raise HTTPException(status_code=503, detail="Bot is not in the correct state.")
|
||||
return None
|
||||
|
||||
|
||||
def is_trading_mode(config=Depends(get_config)):
|
||||
if config["runmode"] not in TRADE_MODES:
|
||||
raise HTTPException(status_code=503, detail="Bot is not in the correct state.")
|
||||
return None
|
||||
|
||||
@@ -21,6 +21,74 @@ from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_TRADE_MODE_ONLY = "*only available in trading mode*"
|
||||
_WEBSERVER_MODE_ONLY = "*only available in webserver mode*"
|
||||
|
||||
_OPENAPI_TAGS = [
|
||||
{"name": "Auth", "description": "Authentication endpoints."},
|
||||
{
|
||||
"name": "Info",
|
||||
"description": ("Information endpoints providing general information about the bot."),
|
||||
},
|
||||
{
|
||||
"name": "Bot-control",
|
||||
"description": (f"Bot control endpoints to start/stop trading - {_TRADE_MODE_ONLY}."),
|
||||
},
|
||||
{
|
||||
"name": "Pairlist",
|
||||
"description": f"Pairlist management - {_TRADE_MODE_ONLY}.",
|
||||
},
|
||||
{
|
||||
"name": "Locks",
|
||||
"description": f"Pair lock management - {_TRADE_MODE_ONLY}.",
|
||||
},
|
||||
{
|
||||
"name": "Candle data",
|
||||
"description": "Candle / OHLCV data.",
|
||||
},
|
||||
{
|
||||
"name": "Trading-info",
|
||||
"description": f"Trading related information - {_TRADE_MODE_ONLY}.",
|
||||
},
|
||||
{
|
||||
"name": "Trades",
|
||||
"description": f"Trade management - {_TRADE_MODE_ONLY}.",
|
||||
},
|
||||
{
|
||||
"name": "Strategy",
|
||||
"description": f"List and retrieve strategies - {_WEBSERVER_MODE_ONLY}.",
|
||||
},
|
||||
{
|
||||
"name": "Hyperopt",
|
||||
"description": f"Retrieve hyperopt loss functions - {_WEBSERVER_MODE_ONLY}.",
|
||||
},
|
||||
{
|
||||
"name": "FreqAI",
|
||||
"description": f"FreqAI related endpoints - {_WEBSERVER_MODE_ONLY}.",
|
||||
},
|
||||
{
|
||||
"name": "Download-data",
|
||||
"description": f"Download data endpoints - {_WEBSERVER_MODE_ONLY}.",
|
||||
},
|
||||
{
|
||||
"name": "Backtest",
|
||||
"description": f"Backtest endpoints - {_WEBSERVER_MODE_ONLY}.",
|
||||
},
|
||||
{
|
||||
"name": "Pairlists",
|
||||
"description": f"Pairlist endpoints - {_WEBSERVER_MODE_ONLY}.",
|
||||
},
|
||||
{
|
||||
"name": "Trading",
|
||||
"description": f"Trading related endpoints - {_TRADE_MODE_ONLY}.",
|
||||
},
|
||||
{
|
||||
"name": "Webserver",
|
||||
"description": (f"Webserver related endpoints - {_WEBSERVER_MODE_ONLY}."),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class FTJSONResponse(JSONResponse):
|
||||
media_type = "application/json"
|
||||
|
||||
@@ -68,6 +136,7 @@ class ApiServer(RPCHandler):
|
||||
docs_url="/docs" if api_config.get("enable_openapi", False) else None,
|
||||
redoc_url=None,
|
||||
default_response_class=FTJSONResponse,
|
||||
openapi_tags=_OPENAPI_TAGS,
|
||||
)
|
||||
self.configure_app(self.app, self._config)
|
||||
self.start_api()
|
||||
@@ -122,28 +191,44 @@ class ApiServer(RPCHandler):
|
||||
from freqtrade.rpc.api_server.api_download_data import router as api_download_data
|
||||
from freqtrade.rpc.api_server.api_pair_history import router as api_pair_history
|
||||
from freqtrade.rpc.api_server.api_pairlists import router as api_pairlists
|
||||
from freqtrade.rpc.api_server.api_trading import router as api_trading
|
||||
from freqtrade.rpc.api_server.api_v1 import router as api_v1
|
||||
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
|
||||
from freqtrade.rpc.api_server.api_webserver import router as api_webserver
|
||||
from freqtrade.rpc.api_server.api_ws import router as ws_router
|
||||
from freqtrade.rpc.api_server.deps import is_webserver_mode
|
||||
from freqtrade.rpc.api_server.deps import is_trading_mode, is_webserver_mode
|
||||
from freqtrade.rpc.api_server.web_ui import router_ui
|
||||
|
||||
app.include_router(api_v1_public, prefix="/api/v1")
|
||||
|
||||
app.include_router(router_login, prefix="/api/v1", tags=["auth"])
|
||||
app.include_router(router_login, prefix="/api/v1", tags=["Auth"])
|
||||
app.include_router(
|
||||
api_v1,
|
||||
prefix="/api/v1",
|
||||
dependencies=[Depends(http_basic_or_jwt_token)],
|
||||
)
|
||||
app.include_router(
|
||||
api_trading,
|
||||
prefix="/api/v1",
|
||||
tags=["Trading"],
|
||||
dependencies=[Depends(http_basic_or_jwt_token), Depends(is_trading_mode)],
|
||||
)
|
||||
app.include_router(
|
||||
api_webserver,
|
||||
prefix="/api/v1",
|
||||
tags=["Webserver"],
|
||||
dependencies=[Depends(http_basic_or_jwt_token), Depends(is_webserver_mode)],
|
||||
)
|
||||
app.include_router(
|
||||
api_backtest,
|
||||
prefix="/api/v1",
|
||||
tags=["Backtest"],
|
||||
dependencies=[Depends(http_basic_or_jwt_token), Depends(is_webserver_mode)],
|
||||
)
|
||||
app.include_router(
|
||||
api_bg_tasks,
|
||||
prefix="/api/v1",
|
||||
tags=["Webserver"],
|
||||
dependencies=[Depends(http_basic_or_jwt_token), Depends(is_webserver_mode)],
|
||||
)
|
||||
app.include_router(
|
||||
@@ -154,11 +239,13 @@ class ApiServer(RPCHandler):
|
||||
app.include_router(
|
||||
api_pairlists,
|
||||
prefix="/api/v1",
|
||||
tags=["Webserver", "Pairlists"],
|
||||
dependencies=[Depends(http_basic_or_jwt_token), Depends(is_webserver_mode)],
|
||||
)
|
||||
app.include_router(
|
||||
api_download_data,
|
||||
prefix="/api/v1",
|
||||
tags=["Download-data", "Webserver"],
|
||||
dependencies=[Depends(http_basic_or_jwt_token), Depends(is_webserver_mode)],
|
||||
)
|
||||
app.include_router(ws_router, prefix="/api/v1")
|
||||
|
||||
@@ -5,7 +5,6 @@ e.g BTC to USD
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
@@ -13,6 +12,7 @@ from freqtrade.constants import SUPPORTED_FIAT, Config
|
||||
from freqtrade.mixins.logging_mixin import LoggingMixin
|
||||
from freqtrade.util import FtTTLCache
|
||||
from freqtrade.util.coin_gecko import FtCoinGeckoApi
|
||||
from freqtrade.util.singleton import SingletonMeta
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -32,26 +32,16 @@ coingecko_mapping = {
|
||||
}
|
||||
|
||||
|
||||
class CryptoToFiatConverter(LoggingMixin):
|
||||
class CryptoToFiatConverter(LoggingMixin, metaclass=SingletonMeta):
|
||||
"""
|
||||
Main class to initiate Crypto to FIAT.
|
||||
This object contains a list of pair Crypto, FIAT
|
||||
This object is also a Singleton
|
||||
"""
|
||||
|
||||
__instance = None
|
||||
|
||||
_coinlistings: list[dict] = []
|
||||
_backoff: float = 0.0
|
||||
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
||||
"""
|
||||
Singleton pattern to ensure only one instance is created.
|
||||
"""
|
||||
if not cls.__instance:
|
||||
cls.__instance = super().__new__(cls)
|
||||
return cls.__instance
|
||||
|
||||
def __init__(self, config: Config) -> None:
|
||||
# Timeout: 6h
|
||||
self._pair_price: FtTTLCache = FtTTLCache(maxsize=500, ttl=6 * 60 * 60)
|
||||
|
||||
+104
-23
@@ -19,7 +19,16 @@ from freqtrade import __version__
|
||||
from freqtrade.configuration.timerange import TimeRange
|
||||
from freqtrade.constants import CANCEL_REASON, DEFAULT_DATAFRAME_COLUMNS, Config
|
||||
from freqtrade.data.history import load_data
|
||||
from freqtrade.data.metrics import DrawDownResult, calculate_expectancy, calculate_max_drawdown
|
||||
from freqtrade.data.metrics import (
|
||||
DrawDownResult,
|
||||
calculate_cagr,
|
||||
calculate_calmar,
|
||||
calculate_expectancy,
|
||||
calculate_max_drawdown,
|
||||
calculate_sharpe,
|
||||
calculate_sortino,
|
||||
calculate_sqn,
|
||||
)
|
||||
from freqtrade.enums import (
|
||||
CandleType,
|
||||
ExitCheckTuple,
|
||||
@@ -689,6 +698,34 @@ class RPC:
|
||||
last_date = trades[-1].open_date_utc if trades else None
|
||||
num = float(len(durations) or 1)
|
||||
bot_start = KeyValueStore.get_datetime_value("bot_start_time")
|
||||
|
||||
sharpe = calculate_sharpe(
|
||||
trades=trades_df,
|
||||
min_date=first_date,
|
||||
max_date=last_date,
|
||||
starting_balance=starting_balance,
|
||||
)
|
||||
sortino = calculate_sortino(
|
||||
trades=trades_df,
|
||||
min_date=first_date,
|
||||
max_date=last_date,
|
||||
starting_balance=starting_balance,
|
||||
)
|
||||
sqn = calculate_sqn(trades=trades_df, starting_balance=starting_balance)
|
||||
calmar = calculate_calmar(
|
||||
trades=trades_df,
|
||||
min_date=first_date,
|
||||
max_date=last_date,
|
||||
starting_balance=starting_balance,
|
||||
)
|
||||
current_balance = self._freqtrade.wallets.get_total_stake_amount()
|
||||
days_passed = max(1, (last_date - first_date).days) if first_date and last_date else 1
|
||||
cagr = calculate_cagr(
|
||||
starting_balance=starting_balance,
|
||||
final_balance=current_balance,
|
||||
days_passed=days_passed,
|
||||
)
|
||||
|
||||
return {
|
||||
"profit_closed_coin": profit_closed_coin_sum,
|
||||
"profit_closed_percent_mean": round(profit_closed_ratio_mean * 100, 2),
|
||||
@@ -725,6 +762,11 @@ class RPC:
|
||||
"winrate": winrate,
|
||||
"expectancy": expectancy,
|
||||
"expectancy_ratio": expectancy_ratio,
|
||||
"sharpe": sharpe,
|
||||
"sortino": sortino,
|
||||
"sqn": sqn,
|
||||
"calmar": calmar,
|
||||
"cagr": cagr,
|
||||
"max_drawdown": drawdown.relative_account_drawdown,
|
||||
"max_drawdown_abs": drawdown.drawdown_abs,
|
||||
"max_drawdown_start": format_date(drawdown.high_date),
|
||||
@@ -804,12 +846,9 @@ class RPC:
|
||||
if is_stake_currency:
|
||||
trade_amount = self._freqtrade.wallets.get_available_stake_amount()
|
||||
|
||||
try:
|
||||
est_stake, est_stake_bot = self.__balance_get_est_stake(
|
||||
coin, stake_currency, trade_amount, balance
|
||||
)
|
||||
except ValueError:
|
||||
continue
|
||||
est_stake, est_stake_bot = self.__balance_get_est_stake(
|
||||
coin, stake_currency, trade_amount, balance
|
||||
)
|
||||
|
||||
total += est_stake
|
||||
|
||||
@@ -832,10 +871,33 @@ class RPC:
|
||||
}
|
||||
)
|
||||
symbol: str
|
||||
position: PositionWallet
|
||||
for symbol, position in self._freqtrade.wallets.get_all_positions().items():
|
||||
total += position.collateral
|
||||
total_bot += position.collateral
|
||||
pos: PositionWallet
|
||||
for symbol, pos in self._freqtrade.wallets.get_all_positions().items():
|
||||
est_stake = pos.collateral
|
||||
pos_base = self._freqtrade.exchange.get_pair_base_currency(symbol)
|
||||
if pos.leverage:
|
||||
try:
|
||||
rate = self._freqtrade.exchange.get_conversion_rate(pos_base, stake_currency)
|
||||
if rate:
|
||||
# For a leveraged position, equity (what we want as est_stake) is:
|
||||
# equity = collateral + unlevered PnL
|
||||
# For longs: unlevered PnL = current_value - open_value
|
||||
# est_stake = rate * pos.position - pos.collateral * (pos.leverage - 1)
|
||||
# For shorts: unlevered PnL = open_value - current_value
|
||||
# est_stake = collateral + (open_value - current_value)
|
||||
# = collateral + (pos.collateral * pos.leverage)
|
||||
# - rate * pos.position
|
||||
if pos.side == "long":
|
||||
est_stake = rate * pos.position - pos.collateral * (pos.leverage - 1)
|
||||
else:
|
||||
est_stake = pos.collateral * (1 + pos.leverage) - rate * pos.position
|
||||
except (ExchangeError, PricingError) as e:
|
||||
logger.warning(f"Error {e} getting rate for futures {symbol} / {pos_base}")
|
||||
pass
|
||||
|
||||
# Add the estimated stake (collateral + unlevered PnL) to totals
|
||||
total += est_stake
|
||||
total_bot += est_stake
|
||||
|
||||
currencies.append(
|
||||
{
|
||||
@@ -843,12 +905,12 @@ class RPC:
|
||||
"free": 0,
|
||||
"balance": 0,
|
||||
"used": 0,
|
||||
"position": position.position,
|
||||
"est_stake": position.collateral,
|
||||
"est_stake_bot": position.collateral,
|
||||
"position": pos.position,
|
||||
"est_stake": est_stake,
|
||||
"est_stake_bot": est_stake,
|
||||
"stake": stake_currency,
|
||||
"side": position.side,
|
||||
"is_bot_managed": True,
|
||||
"side": pos.side,
|
||||
"is_bot_managed": pos_base in open_assets,
|
||||
"is_position": True,
|
||||
}
|
||||
)
|
||||
@@ -940,7 +1002,11 @@ class RPC:
|
||||
return {"status": "Reloaded from orders from exchange"}
|
||||
|
||||
def __exec_force_exit(
|
||||
self, trade: Trade, ordertype: str | None, amount: float | None = None
|
||||
self,
|
||||
trade: Trade,
|
||||
ordertype: str | None,
|
||||
amount: float | None = None,
|
||||
price: float | None = None,
|
||||
) -> bool:
|
||||
# Check if there is there are open orders
|
||||
trade_entry_cancelation_registry = []
|
||||
@@ -964,8 +1030,13 @@ class RPC:
|
||||
# Order cancellation failed, so we can't exit.
|
||||
return False
|
||||
# Get current rate and execute sell
|
||||
current_rate = self._freqtrade.exchange.get_rate(
|
||||
trade.pair, side="exit", is_short=trade.is_short, refresh=True
|
||||
|
||||
current_rate = (
|
||||
self._freqtrade.exchange.get_rate(
|
||||
trade.pair, side="exit", is_short=trade.is_short, refresh=True
|
||||
)
|
||||
if ordertype == "market" or price is None
|
||||
else price
|
||||
)
|
||||
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
|
||||
order_type = ordertype or self._freqtrade.strategy.order_types.get(
|
||||
@@ -983,18 +1054,28 @@ class RPC:
|
||||
sub_amount = amount
|
||||
|
||||
self._freqtrade.execute_trade_exit(
|
||||
trade, current_rate, exit_check, ordertype=order_type, sub_trade_amt=sub_amount
|
||||
trade,
|
||||
current_rate,
|
||||
exit_check,
|
||||
ordertype=order_type,
|
||||
sub_trade_amt=sub_amount,
|
||||
skip_custom_exit_price=price is not None and ordertype == "limit",
|
||||
)
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def _rpc_force_exit(
|
||||
self, trade_id: str, ordertype: str | None = None, *, amount: float | None = None
|
||||
self,
|
||||
trade_id: str,
|
||||
ordertype: str | None = None,
|
||||
*,
|
||||
amount: float | None = None,
|
||||
price: float | None = None,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Handler for forceexit <id>.
|
||||
Sells the given trade at current price
|
||||
exits the given trade. Uses current price if price is None.
|
||||
"""
|
||||
|
||||
if self._freqtrade.state == State.STOPPED:
|
||||
@@ -1024,7 +1105,7 @@ class RPC:
|
||||
logger.warning("force_exit: Invalid argument received")
|
||||
raise RPCException("invalid argument")
|
||||
|
||||
result = self.__exec_force_exit(trade, ordertype, amount)
|
||||
result = self.__exec_force_exit(trade, ordertype, amount, price)
|
||||
Trade.commit()
|
||||
self._freqtrade.wallets.update()
|
||||
if not result:
|
||||
|
||||
@@ -1285,7 +1285,7 @@ class Telegram(RPCHandler):
|
||||
if curr["is_position"]:
|
||||
curr_output = (
|
||||
f"*{curr['currency']}:*\n"
|
||||
f"\t`{curr['side']}: {curr['position']:.8f}`\n"
|
||||
f"\t`{curr['side']}: {round_value(curr['position'], 8)}`\n"
|
||||
f"\t`Est. {curr['stake']}: "
|
||||
f"{fmt_coin(curr['est_stake'], curr['stake'], False)}`\n"
|
||||
)
|
||||
@@ -1296,10 +1296,10 @@ class Telegram(RPCHandler):
|
||||
|
||||
curr_output = (
|
||||
f"*{curr['currency']}:*\n"
|
||||
f"\t`Available: {curr['free']:.8f}`\n"
|
||||
f"\t`Balance: {curr['balance']:.8f}`\n"
|
||||
f"\t`Pending: {curr['used']:.8f}`\n"
|
||||
f"\t`Bot Owned: {curr['bot_owned']:.8f}`\n"
|
||||
f"\t`Available: {fmt_coin(curr['free'], curr['currency'], False)}`\n"
|
||||
f"\t`Balance: {fmt_coin(curr['balance'], curr['currency'], False)}`\n"
|
||||
f"\t`Pending: {fmt_coin(curr['used'], curr['currency'], False)}`\n"
|
||||
f"\t`Bot Owned: {fmt_coin(curr['bot_owned'], curr['currency'], False)}`\n"
|
||||
f"\t`Est. {curr['stake']}: {est_stake}`\n"
|
||||
)
|
||||
|
||||
|
||||
@@ -104,8 +104,11 @@ def _create_and_merge_informative_pair(
|
||||
):
|
||||
asset = inf_data.asset or ""
|
||||
timeframe = inf_data.timeframe
|
||||
timeframe1 = inf_data.timeframe
|
||||
fmt = inf_data.fmt
|
||||
candle_type = inf_data.candle_type
|
||||
if candle_type == CandleType.FUNDING_RATE:
|
||||
timeframe1 = strategy.dp.get_funding_rate_timeframe()
|
||||
|
||||
config = strategy.config
|
||||
|
||||
@@ -132,10 +135,10 @@ def _create_and_merge_informative_pair(
|
||||
fmt = "{base}_{quote}_" + fmt # Informatives of other pairs
|
||||
|
||||
inf_metadata = {"pair": asset, "timeframe": timeframe}
|
||||
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe, candle_type)
|
||||
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe1, candle_type)
|
||||
if inf_dataframe.empty:
|
||||
raise ValueError(
|
||||
f"Informative dataframe for ({asset}, {timeframe}, {candle_type}) is empty. "
|
||||
f"Informative dataframe for ({asset}, {timeframe1}, {candle_type}) is empty. "
|
||||
"Can't populate informative indicators."
|
||||
)
|
||||
inf_dataframe = populate_indicators_fn(strategy, inf_dataframe, inf_metadata)
|
||||
@@ -163,7 +166,7 @@ def _create_and_merge_informative_pair(
|
||||
dataframe,
|
||||
inf_dataframe,
|
||||
strategy.timeframe,
|
||||
timeframe,
|
||||
timeframe1,
|
||||
ffill=inf_data.ffill,
|
||||
append_timeframe=False,
|
||||
date_column=date_column,
|
||||
|
||||
@@ -1718,7 +1718,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
timeout_unit = self.config.get("unfilledtimeout", {}).get("unit", "minutes")
|
||||
timeout_kwargs = {timeout_unit: -timeout}
|
||||
timeout_threshold = current_time + timedelta(**timeout_kwargs)
|
||||
timedout = order.status == "open" and order.order_date_utc < timeout_threshold
|
||||
timedout = order.status == "open" and order.order_date_utc <= timeout_threshold
|
||||
if timedout:
|
||||
return True
|
||||
time_method = (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user