Compare commits
467 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c8c53cff8 | |||
| 846b7734e1 | |||
| c72d18270e | |||
| 02836368fb | |||
| d282027858 | |||
| d329ad28c2 | |||
| 2de86af51e | |||
| 97780ee5bf | |||
| e162f98cf5 | |||
| c6967b11be | |||
| 1e5d353cf7 | |||
| 17a538b5ba | |||
| fc746214f6 | |||
| 58432c1cfa | |||
| 69119764bf | |||
| a20e641bcd | |||
| ad68eff8f4 | |||
| 68552a61bd | |||
| b7419305d4 | |||
| 8cc2c5010a | |||
| 872154a6fc | |||
| c8ee8e82f9 | |||
| 5cbcf72501 | |||
| 35876cf8ee | |||
| ac60871ff9 | |||
| 866e34a983 | |||
| 7107971820 | |||
| 3484590640 | |||
| 971b7d074c | |||
| ec89aad10b | |||
| fc13a99b33 | |||
| 03fb204408 | |||
| 0a96aa69b9 | |||
| ddbfce01b5 | |||
| 12e75e849d | |||
| 916a847639 | |||
| 4769b50709 | |||
| 18dd9cf803 | |||
| def204448a | |||
| 63e652619f | |||
| 3cb4b10656 | |||
| 23c3757c7a | |||
| b20aee7653 | |||
| 4d7c4352a7 | |||
| a5c914951f | |||
| 9ab08d513a | |||
| 87ce7dc02c | |||
| 3c7c4bc02f | |||
| 0484b891f1 | |||
| 71689d9d6f | |||
| 881247a823 | |||
| d26154f994 | |||
| 0e14dd6190 | |||
| 3676514ba6 | |||
| 9ec23a0292 | |||
| 1272a15c35 | |||
| a3a175e332 | |||
| 71b777ef34 | |||
| c4cd547451 | |||
| b4e8122fd3 | |||
| b1393f1bd4 | |||
| 34c5c68a91 | |||
| c9a018cf0e | |||
| 0333abcbdc | |||
| 0440b3ef75 | |||
| d9ed8c6560 | |||
| 2f2467256c | |||
| 26f0fe2383 | |||
| 5e3da30ab9 | |||
| 651d2c06b9 | |||
| b371593084 | |||
| 3096cddbcf | |||
| 5bbe6aacfd | |||
| fe4bf271b4 | |||
| 0d3d1e6d9a | |||
| 57e01329ce | |||
| 99359a7f85 | |||
| 5f0932f5ed | |||
| fece88e7cd | |||
| c8eb2523ac | |||
| 964b439119 | |||
| ee4f4f2d0d | |||
| 7c215688c0 | |||
| 087ca2bd4a | |||
| 6193df36a3 | |||
| 893b43766f | |||
| a70ca6f612 | |||
| e5c783d2db | |||
| e1b652d890 | |||
| 4e98b5f710 | |||
| 901b3f6fe7 | |||
| d0a9d927dd | |||
| 16eb95c71d | |||
| 054ed0a7b6 | |||
| 82853e37a1 | |||
| 81c6336e11 | |||
| 6404bd8b2b | |||
| 9a9a6eaa63 | |||
| 67cdab6ee9 | |||
| 546ea430c9 | |||
| 4cc4f0ffa2 | |||
| 58470ba9d0 | |||
| c580e8783f | |||
| bc1ad3acbd | |||
| d2e328b8bd | |||
| 93e9c4fa65 | |||
| 8d231dbf31 | |||
| b178cc5f31 | |||
| 6ee0f16e4f | |||
| 1d701967b7 | |||
| d6b43c5eed | |||
| 3670699ea2 | |||
| 9b11091fc7 | |||
| 58dfffe87a | |||
| e573409925 | |||
| e3fda16d16 | |||
| 71d3572c27 | |||
| 62667d123f | |||
| cfd5af67ca | |||
| 47a952e41c | |||
| c9f43587ec | |||
| e79a58e1c4 | |||
| c62e52c759 | |||
| 5ab61d9da4 | |||
| 55efaec83d | |||
| c5c2c8c185 | |||
| 755ce3a858 | |||
| 105cd99395 | |||
| db7799d2fb | |||
| 30f94ef5b7 | |||
| 6c259ddca1 | |||
| 0f82ea8767 | |||
| eac5d53a64 | |||
| 4b43ccae11 | |||
| 8a2763d275 | |||
| 4b3230689c | |||
| aa86c49640 | |||
| 1adc8836dd | |||
| 92993c0c4c | |||
| 9b2c116d9b | |||
| 2ad85a6957 | |||
| a0e92f5a47 | |||
| 17f0697c43 | |||
| 199e3f91f2 | |||
| cc14cf23b1 | |||
| 327f750ead | |||
| a304080c08 | |||
| 020d4b5b4d | |||
| 78cb1e608f | |||
| b84a2fcac7 | |||
| c0119d7baa | |||
| 0bd513012a | |||
| 3f6cd9ee51 | |||
| 8964c138f1 | |||
| ef23f0fcba | |||
| 074343f0f1 | |||
| 966eb59fd3 | |||
| 778bd7b3b0 | |||
| b4cd46d6db | |||
| 1d73b57e8c | |||
| 476703bf76 | |||
| cd09d3567f | |||
| 4c25e9d21a | |||
| dd08c134b4 | |||
| aa8bb27cf0 | |||
| d428d48393 | |||
| 3bdfa85237 | |||
| 95a824fbe7 | |||
| bb85efd6ad | |||
| 7321a14c35 | |||
| 2745a5d334 | |||
| d123d3bb82 | |||
| b14873400d | |||
| d9b95a4c27 | |||
| 71178ff1d2 | |||
| d3d9f3281e | |||
| 3b4b833dd0 | |||
| fcc87399cd | |||
| 654f8a7573 | |||
| 2fdb9663d7 | |||
| 3739a10735 | |||
| 9a425af76f | |||
| 3c48208b31 | |||
| 68db8d0201 | |||
| 2207773678 | |||
| cbe6d52a73 | |||
| b7fc0190f7 | |||
| 4061eaf888 | |||
| ef042ae5ec | |||
| 6ee792069d | |||
| 46c81d7018 | |||
| caac77c90b | |||
| af05e3d747 | |||
| 59a287106d | |||
| 8b5e6bf9be | |||
| d9882978d5 | |||
| 04f2b7bad6 | |||
| 8597709b2e | |||
| 2fea75e312 | |||
| 511e4d16d5 | |||
| e0f780f10e | |||
| 44689d058b | |||
| 1431b2b44a | |||
| 106bda51d4 | |||
| 90e4eb59b2 | |||
| 701f6fc050 | |||
| 0235db48a8 | |||
| e9e7bf3caf | |||
| 4464b02719 | |||
| 4ed9ffbf31 | |||
| c017366086 | |||
| 34bce4d251 | |||
| 6754072bca | |||
| 15771043f7 | |||
| b417a0297b | |||
| 5daafaabc2 | |||
| 08db2f05aa | |||
| 0654186400 | |||
| d85518ccb2 | |||
| c34cb9eb12 | |||
| cb01a46089 | |||
| 0bcbb28d51 | |||
| e2a47ee95a | |||
| 36836ea803 | |||
| 19d9adaf9c | |||
| cf078f8093 | |||
| 5ddca4e9f9 | |||
| 6db66b1e58 | |||
| 0a903e45f2 | |||
| d0a2b9403e | |||
| a18c85ec64 | |||
| 0914b8b5f4 | |||
| cb2a871538 | |||
| ef877b5fcc | |||
| 2738f3e437 | |||
| 79f765a930 | |||
| 696f8b308a | |||
| 4a2473e7e9 | |||
| a0caa7c5a2 | |||
| 388a0c605c | |||
| 55e4d08fd8 | |||
| 7c00a4ed13 | |||
| 793cc1acc0 | |||
| 33d910ccea | |||
| 8a44337c30 | |||
| 35871d7b4b | |||
| 786d4e03fc | |||
| 5e5b56e445 | |||
| 384ebc5b38 | |||
| 8567af8d28 | |||
| 88e818926a | |||
| 4bcb4fdd82 | |||
| 0b3a6aa9de | |||
| 12ea1cde80 | |||
| df73b8288c | |||
| a50291cd90 | |||
| be8ea685de | |||
| d050beb627 | |||
| 1380cadf15 | |||
| 72eeb6561c | |||
| eeb460e55c | |||
| e8d0b01991 | |||
| 94020a664b | |||
| 8c5194d5e2 | |||
| 67e81c9018 | |||
| 25ebdb4cb3 | |||
| 1657fd1f0f | |||
| 63e702a3bf | |||
| e9f21d0209 | |||
| a044649eef | |||
| 48317c2307 | |||
| d598c91de3 | |||
| 5003c2af21 | |||
| 6963a92332 | |||
| e2863e1620 | |||
| 869756d87d | |||
| 203890844b | |||
| ba76b87952 | |||
| fbab18b081 | |||
| f7178ecd6f | |||
| 59cbac7147 | |||
| fbc70a60f7 | |||
| b45365d385 | |||
| 9990780e6b | |||
| 65df2f30c1 | |||
| e7aa706f73 | |||
| 3619247123 | |||
| 047dac7b7d | |||
| 7646849405 | |||
| 10ae0ed9c1 | |||
| e060e729df | |||
| d81bbba4c2 | |||
| d52936fd42 | |||
| 21a5abf25d | |||
| c3980a2746 | |||
| 2cadbd1229 | |||
| 0f57291aa7 | |||
| 0e9169e1ec | |||
| 8680b5faa0 | |||
| 03149cfa42 | |||
| 4eb4732de7 | |||
| 3a5b22c397 | |||
| c3a5d4b4e4 | |||
| b97ff3a784 | |||
| 806af7d796 | |||
| 3c25737f38 | |||
| bf98851dc1 | |||
| 202b72fd8e | |||
| 6f60117319 | |||
| 7070ec30e4 | |||
| bf77019d1c | |||
| b776e5a8e0 | |||
| ca208305a0 | |||
| 972c5c327c | |||
| 80edfd1531 | |||
| 58cd455642 | |||
| caf59d25e8 | |||
| 7df32a34a0 | |||
| 48097f4a7d | |||
| ca3f349d84 | |||
| 0815bb6244 | |||
| 835cb54876 | |||
| 08c2765633 | |||
| f9202c3f6b | |||
| eb186e22d6 | |||
| cbc323875b | |||
| 736b583ff4 | |||
| 757ec53877 | |||
| 7814927dd1 | |||
| 5f8ff373ab | |||
| f286cb0a90 | |||
| 59bd1a1f1f | |||
| e52f6ca6c0 | |||
| e6663c0304 | |||
| ced92300e0 | |||
| f9d061b648 | |||
| e6661a33f9 | |||
| d2afe281bf | |||
| c237aa00b8 | |||
| 23efa4c9c3 | |||
| 8b028734c2 | |||
| 51feeddc5c | |||
| af3e42f59d | |||
| 34d58eb7d5 | |||
| ca0a04fba5 | |||
| 84b3f9924a | |||
| 50b264c6f8 | |||
| 88e8a9fe2b | |||
| d65aef72e8 | |||
| 8273c0c2cd | |||
| c7624b1ed6 | |||
| 54536a1c8e | |||
| 7daa06a207 | |||
| da647735b6 | |||
| 7d0ecfde93 | |||
| 2ef716e94c | |||
| de68850d28 | |||
| 59cefda283 | |||
| babcd5887f | |||
| c9a5e5a98c | |||
| 51dc5811c2 | |||
| 05e36f7b21 | |||
| 0738cae4a0 | |||
| da3c42bbbc | |||
| e728e3bc75 | |||
| 315783441d | |||
| 62f1421cfb | |||
| 52f12b704b | |||
| ee5051f788 | |||
| b30b1227e3 | |||
| dd2f8db392 | |||
| 5a76d81529 | |||
| b9851c78ad | |||
| 777cf2ae7a | |||
| 3729b3d793 | |||
| 39fc78205d | |||
| 46a67e8d4f | |||
| 69017805c0 | |||
| bcec611973 | |||
| fbb11a394a | |||
| 5cb916977e | |||
| 7d4c9bc0cf | |||
| ea2beb46e7 | |||
| 216ee8117a | |||
| 9c1210b34e | |||
| 921e034266 | |||
| 6f358b16c0 | |||
| 264ab2c471 | |||
| a9d6f0023a | |||
| 31af1d5dc7 | |||
| 588bbb45b5 | |||
| 278c999f1f | |||
| 825ae15109 | |||
| 30cacc7cca | |||
| 7ed037bad7 | |||
| 77b00b2a12 | |||
| 773bc386f7 | |||
| 90dacaca09 | |||
| 3689ad16b5 | |||
| ab9954350e | |||
| 533f11d9eb | |||
| 551033a7c3 | |||
| e003930e18 | |||
| 2efef72ec2 | |||
| eb40fc698d | |||
| 751db2be78 | |||
| 6ea353447e | |||
| 5a3839320d | |||
| 555f4b51e1 | |||
| be82248e01 | |||
| 7bed7801cc | |||
| eb409de916 | |||
| 8ce39a6d75 | |||
| 72dc65cb6a | |||
| c94c667fb1 | |||
| 2a0175f629 | |||
| cd68173440 | |||
| c920e3a031 | |||
| 83afc4ac7e | |||
| 19f62649ea | |||
| e4186cde9e | |||
| 61ed6aee62 | |||
| 051b6e9458 | |||
| 44a24685ac | |||
| 1cde980c08 | |||
| 05ef4f9b6f | |||
| 53f00f248e | |||
| a9cd282500 | |||
| 26aa14dbfc | |||
| 04cd931cc8 | |||
| b8a6330c3f | |||
| 949c3c660b | |||
| 9297a90d7f | |||
| b19f17fdfa | |||
| bbdc6c0f5c | |||
| 528c65c194 | |||
| a932df7320 | |||
| 2d9b05a3ab | |||
| 7cf1d25dc6 | |||
| 19c955d505 | |||
| bd243da47e | |||
| bf24c3b279 | |||
| e9c72acff2 | |||
| 09fd22c2f8 | |||
| 45721ed62e | |||
| 5182045cda | |||
| 51ab8fecfc | |||
| b36907133c | |||
| b6d899344e | |||
| 095f9fc1ab | |||
| 0ab9eb7b05 | |||
| d0c582e137 | |||
| d3d4464983 | |||
| 1d0934074c | |||
| 9382fb3be8 | |||
| 223af95da7 | |||
| 8b68b55de4 | |||
| 99592d1a12 | |||
| b69f5aaac3 | |||
| 0c1fefe6e7 | |||
| a049ff9371 | |||
| 0c51e2637f | |||
| 7492b75e1c | |||
| 24dfd9b93b | |||
| 7d65b3e1b4 | |||
| cf43427be5 | |||
| 7ae41be975 |
+93
-58
@@ -31,7 +31,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
@@ -44,7 +44,6 @@ jobs:
|
|||||||
|
|
||||||
- name: pip cache (linux)
|
- name: pip cache (linux)
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
if: runner.os == 'Linux'
|
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||||
@@ -55,7 +54,6 @@ jobs:
|
|||||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||||
|
|
||||||
- name: Installation - *nix
|
- name: Installation - *nix
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip wheel
|
python -m pip install --upgrade pip wheel
|
||||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||||
@@ -90,7 +88,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Backtesting (multi)
|
- name: Backtesting (multi)
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp tests/testdata/config.tests.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade new-strategy -s AwesomeStrategy
|
freqtrade new-strategy -s AwesomeStrategy
|
||||||
freqtrade new-strategy -s AwesomeStrategyMin --template minimal
|
freqtrade new-strategy -s AwesomeStrategyMin --template minimal
|
||||||
@@ -98,7 +96,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Hyperopt
|
- name: Hyperopt
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp tests/testdata/config.tests.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
@@ -122,18 +120,18 @@ jobs:
|
|||||||
details: Freqtrade CI failed on ${{ matrix.os }}
|
details: Freqtrade CI failed on ${{ matrix.os }}
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
build_macos:
|
build-macos:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos-latest ]
|
os: [ "macos-latest", "macos-13" ]
|
||||||
python-version: ["3.9", "3.10", "3.11"]
|
python-version: ["3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -143,14 +141,13 @@ jobs:
|
|||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: ~/dependencies/
|
path: ~/dependencies/
|
||||||
key: ${{ runner.os }}-dependencies
|
key: ${{ matrix.os }}-dependencies
|
||||||
|
|
||||||
- name: pip cache (macOS)
|
- name: pip cache (macOS)
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
if: runner.os == 'macOS'
|
|
||||||
with:
|
with:
|
||||||
path: ~/Library/Caches/pip
|
path: ~/Library/Caches/pip
|
||||||
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
key: ${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||||
|
|
||||||
- name: TA binary *nix
|
- name: TA binary *nix
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
@@ -158,7 +155,6 @@ jobs:
|
|||||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||||
|
|
||||||
- name: Installation - macOS
|
- name: Installation - macOS
|
||||||
if: runner.os == 'macOS'
|
|
||||||
run: |
|
run: |
|
||||||
# brew update
|
# brew update
|
||||||
# TODO: Should be the brew upgrade
|
# TODO: Should be the brew upgrade
|
||||||
@@ -166,16 +162,21 @@ jobs:
|
|||||||
# https://github.com/actions/runner-images/issues/6817
|
# https://github.com/actions/runner-images/issues/6817
|
||||||
rm /usr/local/bin/2to3 || true
|
rm /usr/local/bin/2to3 || true
|
||||||
rm /usr/local/bin/2to3-3.11 || true
|
rm /usr/local/bin/2to3-3.11 || true
|
||||||
|
rm /usr/local/bin/2to3-3.12 || true
|
||||||
rm /usr/local/bin/idle3 || true
|
rm /usr/local/bin/idle3 || true
|
||||||
rm /usr/local/bin/idle3.11 || true
|
rm /usr/local/bin/idle3.11 || true
|
||||||
|
rm /usr/local/bin/idle3.12 || true
|
||||||
rm /usr/local/bin/pydoc3 || true
|
rm /usr/local/bin/pydoc3 || true
|
||||||
rm /usr/local/bin/pydoc3.11 || true
|
rm /usr/local/bin/pydoc3.11 || true
|
||||||
|
rm /usr/local/bin/pydoc3.12 || true
|
||||||
rm /usr/local/bin/python3 || true
|
rm /usr/local/bin/python3 || true
|
||||||
rm /usr/local/bin/python3.11 || true
|
rm /usr/local/bin/python3.11 || true
|
||||||
|
rm /usr/local/bin/python3.12 || true
|
||||||
rm /usr/local/bin/python3-config || true
|
rm /usr/local/bin/python3-config || true
|
||||||
rm /usr/local/bin/python3.11-config || true
|
rm /usr/local/bin/python3.11-config || true
|
||||||
|
rm /usr/local/bin/python3.12-config || true
|
||||||
|
|
||||||
brew install hdf5 c-blosc
|
brew install hdf5 c-blosc libomp
|
||||||
python -m pip install --upgrade pip wheel
|
python -m pip install --upgrade pip wheel
|
||||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||||
@@ -200,14 +201,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Backtesting
|
- name: Backtesting
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp tests/testdata/config.tests.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade new-strategy -s AwesomeStrategyAdv --template advanced
|
freqtrade new-strategy -s AwesomeStrategyAdv --template advanced
|
||||||
freqtrade backtesting --datadir tests/testdata --strategy AwesomeStrategyAdv
|
freqtrade backtesting --datadir tests/testdata --strategy AwesomeStrategyAdv
|
||||||
|
|
||||||
- name: Hyperopt
|
- name: Hyperopt
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp tests/testdata/config.tests.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
@@ -231,7 +232,7 @@ jobs:
|
|||||||
details: Test Succeeded!
|
details: Test Succeeded!
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
build_windows:
|
build-windows:
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
@@ -243,7 +244,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
@@ -275,13 +276,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Backtesting
|
- name: Backtesting
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp tests/testdata/config.tests.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
||||||
|
|
||||||
- name: Hyperopt
|
- name: Hyperopt
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp tests/testdata/config.tests.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
@@ -301,13 +302,13 @@ jobs:
|
|||||||
details: Test Failed
|
details: Test Failed
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
mypy_version_check:
|
mypy-version-check:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
@@ -321,12 +322,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
- uses: pre-commit/action@v3.0.0
|
- uses: pre-commit/action@v3.0.0
|
||||||
|
|
||||||
docs_check:
|
docs-check:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -336,7 +337,7 @@ jobs:
|
|||||||
./tests/test_docs.sh
|
./tests/test_docs.sh
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
@@ -362,9 +363,9 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.9"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Cache_dependencies
|
- name: Cache_dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
@@ -375,7 +376,6 @@ jobs:
|
|||||||
|
|
||||||
- name: pip cache (linux)
|
- name: pip cache (linux)
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
if: runner.os == 'Linux'
|
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||||
@@ -386,7 +386,6 @@ jobs:
|
|||||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||||
|
|
||||||
- name: Installation - *nix
|
- name: Installation - *nix
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip wheel
|
python -m pip install --upgrade pip wheel
|
||||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||||
@@ -399,17 +398,17 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI_WEB_PROXY: http://152.67.78.211:13128
|
CI_WEB_PROXY: http://152.67.78.211:13128
|
||||||
run: |
|
run: |
|
||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun
|
pytest --random-order --longrun --durations 20 -n auto --dist loadscope
|
||||||
|
|
||||||
|
|
||||||
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
||||||
notify-complete:
|
notify-complete:
|
||||||
needs: [
|
needs: [
|
||||||
build_linux,
|
build_linux,
|
||||||
build_macos,
|
build-macos,
|
||||||
build_windows,
|
build-windows,
|
||||||
docs_check,
|
docs-check,
|
||||||
mypy_version_check,
|
mypy-version-check,
|
||||||
pre-commit,
|
pre-commit,
|
||||||
build_linux_online
|
build_linux_online
|
||||||
]
|
]
|
||||||
@@ -436,8 +435,63 @@ jobs:
|
|||||||
details: Test Completed!
|
details: Test Completed!
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
deploy:
|
build:
|
||||||
needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check, pre-commit ]
|
name: "Build"
|
||||||
|
needs: [ build_linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Build distribution
|
||||||
|
run: |
|
||||||
|
pip install -U build
|
||||||
|
python -m build --sdist --wheel
|
||||||
|
|
||||||
|
- name: Upload artifacts 📦
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: freqtrade-build
|
||||||
|
path: |
|
||||||
|
dist
|
||||||
|
retention-days: 10
|
||||||
|
|
||||||
|
deploy-pypi:
|
||||||
|
name: "Deploy to PyPI"
|
||||||
|
needs: [ build ]
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
if: (github.event_name == 'release')
|
||||||
|
environment:
|
||||||
|
name: release
|
||||||
|
url: https://pypi.org/p/freqtrade
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download artifact 📦
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: freqtrade-build
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Publish to PyPI (Test)
|
||||||
|
uses: pypa/gh-action-pypi-publish@v1.8.11
|
||||||
|
with:
|
||||||
|
repository-url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
|
- name: Publish to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@v1.8.11
|
||||||
|
|
||||||
|
|
||||||
|
deploy-docker:
|
||||||
|
needs: [ build_linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||||
@@ -446,7 +500,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
@@ -455,26 +509,6 @@ jobs:
|
|||||||
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF##*/})"
|
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF##*/})"
|
||||||
id: extract_branch
|
id: extract_branch
|
||||||
|
|
||||||
- name: Build distribution
|
|
||||||
run: |
|
|
||||||
pip install -U setuptools wheel
|
|
||||||
python setup.py sdist bdist_wheel
|
|
||||||
|
|
||||||
- name: Publish to PyPI (Test)
|
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.10
|
|
||||||
if: (github.event_name == 'release')
|
|
||||||
with:
|
|
||||||
user: __token__
|
|
||||||
password: ${{ secrets.pypi_test_password }}
|
|
||||||
repository_url: https://test.pypi.org/legacy/
|
|
||||||
|
|
||||||
- name: Publish to PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.10
|
|
||||||
if: (github.event_name == 'release')
|
|
||||||
with:
|
|
||||||
user: __token__
|
|
||||||
password: ${{ secrets.pypi_password }}
|
|
||||||
|
|
||||||
- name: Dockerhub login
|
- name: Dockerhub login
|
||||||
env:
|
env:
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
@@ -506,10 +540,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
build_helpers/publish_docker_multi.sh
|
build_helpers/publish_docker_multi.sh
|
||||||
|
|
||||||
deploy_arm:
|
deploy-arm:
|
||||||
|
name: "Deploy Docker"
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
needs: [ deploy ]
|
needs: [ deploy-docker ]
|
||||||
# Only run on 64bit machines
|
# Only run on 64bit machines
|
||||||
runs-on: [self-hosted, linux, ARM64]
|
runs-on: [self-hosted, linux, ARM64]
|
||||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Pre-commit auto-update
|
||||||
|
|
||||||
|
on:
|
||||||
|
# every day at midnight
|
||||||
|
schedule:
|
||||||
|
- cron: "0 3 * * 2"
|
||||||
|
# on demand
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|
||||||
|
- name: Install pre-commit
|
||||||
|
run: pip install pre-commit
|
||||||
|
|
||||||
|
- name: Run auto-update
|
||||||
|
run: pre-commit autoupdate
|
||||||
|
|
||||||
|
- name: Run pre-commit
|
||||||
|
run: pre-commit run --all-files
|
||||||
|
|
||||||
|
- uses: peter-evans/create-pull-request@v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||||
|
add-paths: .pre-commit-config.yaml
|
||||||
|
labels: |
|
||||||
|
Tech maintenance
|
||||||
|
branch: update/pre-commit-hooks
|
||||||
|
title: Update pre-commit hooks
|
||||||
|
commit-message: "chore: update pre-commit hooks"
|
||||||
|
committer: Freqtrade Bot <noreply@github.com>
|
||||||
|
body: Update versions of pre-commit hooks to latest version.
|
||||||
|
delete-branch: true
|
||||||
@@ -111,7 +111,6 @@ target/
|
|||||||
#exceptions
|
#exceptions
|
||||||
!*.gitkeep
|
!*.gitkeep
|
||||||
!config_examples/config_binance.example.json
|
!config_examples/config_binance.example.json
|
||||||
!config_examples/config_bittrex.example.json
|
|
||||||
!config_examples/config_full.example.json
|
!config_examples/config_full.example.json
|
||||||
!config_examples/config_kraken.example.json
|
!config_examples/config_kraken.example.json
|
||||||
!config_examples/config_freqai.example.json
|
!config_examples/config_freqai.example.json
|
||||||
|
|||||||
@@ -2,27 +2,28 @@
|
|||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pycqa/flake8
|
- repo: https://github.com/pycqa/flake8
|
||||||
rev: "6.0.0"
|
rev: "6.1.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
|
additional_dependencies: [Flake8-pyproject]
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v1.5.1"
|
rev: "v1.8.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.3.0.6
|
- types-cachetools==5.3.0.7
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.31.0.10
|
- types-requests==2.31.0.10
|
||||||
- types-tabulate==0.9.0.3
|
- types-tabulate==0.9.0.3
|
||||||
- types-python-dateutil==2.8.19.14
|
- types-python-dateutil==2.8.19.14
|
||||||
- SQLAlchemy==2.0.22
|
- SQLAlchemy==2.0.23
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
rev: "5.12.0"
|
rev: "5.13.2"
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
name: isort (python)
|
name: isort (python)
|
||||||
@@ -30,12 +31,12 @@ repos:
|
|||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.1.1'
|
rev: 'v0.1.9'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.4.0
|
rev: v4.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: |
|
exclude: |
|
||||||
|
|||||||
+1
-1
@@ -125,7 +125,7 @@ Exceptions:
|
|||||||
|
|
||||||
Contributors may be given commit privileges. Preference will be given to those with:
|
Contributors may be given commit privileges. Preference will be given to those with:
|
||||||
|
|
||||||
1. Past contributions to Freqtrade and other related open-source projects. Contributions to Freqtrade include both code (both accepted and pending) and friendly participation in the issue tracker and Pull request reviews. Quantity and quality are considered.
|
1. Past contributions to Freqtrade and other related open-source projects. Contributions to Freqtrade include both code (both accepted and pending) and friendly participation in the issue tracker and Pull request reviews. Both quantity and quality are considered.
|
||||||
1. A coding style that the other core committers find simple, minimal, and clean.
|
1. A coding style that the other core committers find simple, minimal, and clean.
|
||||||
1. Access to resources for cross-platform development and testing.
|
1. Access to resources for cross-platform development and testing.
|
||||||
1. Time to devote to the project regularly.
|
1. Time to devote to the project regularly.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11.5-slim-bullseye as base
|
FROM python:3.11.6-slim-bookworm as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ recursive-include freqtrade/templates/ *.j2 *.ipynb
|
|||||||
include freqtrade/exchange/binance_leverage_tiers.json
|
include freqtrade/exchange/binance_leverage_tiers.json
|
||||||
include freqtrade/rpc/api_server/ui/fallback_file.html
|
include freqtrade/rpc/api_server/ui/fallback_file.html
|
||||||
include freqtrade/rpc/api_server/ui/favicon.ico
|
include freqtrade/rpc/api_server/ui/favicon.ico
|
||||||
|
|
||||||
|
prune tests
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ hesitate to read the source code and understand the mechanism of this bot.
|
|||||||
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 eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/)
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bitmart](https://bitmart.com/)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
- [X] [Huobi](http://huobi.com/)
|
- [X] [Huobi](http://huobi.com/)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -54,7 +54,7 @@ docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
|||||||
docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
|
docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
|
||||||
|
|
||||||
# Run backtest
|
# Run backtest
|
||||||
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
docker run --rm -v $(pwd)/tests/testdata/config.tests.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "failed running backtest"
|
echo "failed running backtest"
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI
|
|||||||
docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_RL
|
docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_RL
|
||||||
|
|
||||||
# Run backtest
|
# Run backtest
|
||||||
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
docker run --rm -v $(pwd)/tests/testdata/config.tests.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "failed running backtest"
|
echo "failed running backtest"
|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"max_open_trades": 3,
|
"max_open_trades": 3,
|
||||||
"stake_currency": "BTC",
|
"stake_currency": "USDT",
|
||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
"tradable_balance_ratio": 0.99,
|
"tradable_balance_ratio": 0.99,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
@@ -36,21 +36,21 @@
|
|||||||
"ccxt_async_config": {
|
"ccxt_async_config": {
|
||||||
},
|
},
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ALGO/BTC",
|
"ALGO/USDT",
|
||||||
"ATOM/BTC",
|
"ATOM/USDT",
|
||||||
"BAT/BTC",
|
"BAT/USDT",
|
||||||
"BCH/BTC",
|
"BCH/USDT",
|
||||||
"BRD/BTC",
|
"BRD/USDT",
|
||||||
"EOS/BTC",
|
"EOS/USDT",
|
||||||
"ETH/BTC",
|
"ETH/USDT",
|
||||||
"IOTA/BTC",
|
"IOTA/USDT",
|
||||||
"LINK/BTC",
|
"LINK/USDT",
|
||||||
"LTC/BTC",
|
"LTC/USDT",
|
||||||
"NEO/BTC",
|
"NEO/USDT",
|
||||||
"NXS/BTC",
|
"NXS/USDT",
|
||||||
"XMR/BTC",
|
"XMR/USDT",
|
||||||
"XRP/BTC",
|
"XRP/USDT",
|
||||||
"XTZ/BTC"
|
"XTZ/USDT"
|
||||||
],
|
],
|
||||||
"pair_blacklist": [
|
"pair_blacklist": [
|
||||||
"BNB/.*"
|
"BNB/.*"
|
||||||
|
|||||||
+7
-10
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.9.16-slim-bullseye as base
|
FROM python:3.11.6-slim-bookworm as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
@@ -11,34 +11,31 @@ ENV FT_APP_ENV="docker"
|
|||||||
# Prepare environment
|
# Prepare environment
|
||||||
RUN mkdir /freqtrade \
|
RUN mkdir /freqtrade \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-dev libutf8proc-dev libsnappy-dev \
|
&& apt-get -y install sudo libatlas3-base libopenblas-dev curl sqlite3 libhdf5-dev libutf8proc-dev libsnappy-dev \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& useradd -u 1000 -G sudo -U -m ftuser \
|
&& useradd -u 1000 -G sudo -U -m ftuser \
|
||||||
&& chown ftuser:ftuser /freqtrade \
|
&& chown ftuser:ftuser /freqtrade \
|
||||||
# Allow sudoers
|
# 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
|
WORKDIR /freqtrade
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
FROM base as python-deps
|
FROM base as python-deps
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install build-essential libssl-dev libffi-dev libopenblas-dev libgfortran5 pkg-config cmake gcc \
|
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& pip install --upgrade pip \
|
|
||||||
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf
|
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf
|
||||||
|
|
||||||
# Install TA-lib
|
# Install TA-lib
|
||||||
COPY build_helpers/* /tmp/
|
COPY build_helpers/* /tmp/
|
||||||
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
|
|
||||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
||||||
USER ftuser
|
USER ftuser
|
||||||
RUN pip install --user --no-cache-dir numpy==1.25.2 \
|
RUN pip install --user --no-cache-dir numpy \
|
||||||
&& pip install --user /tmp/pyarrow-*.whl \
|
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib==0.4.28 \
|
||||||
&& pip install --user --no-build-isolation TA-Lib==0.4.28 \
|
|
||||||
&& pip install --user --no-cache-dir -r requirements.txt
|
&& pip install --user --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy dependencies to runtime-image
|
# Copy dependencies to runtime-image
|
||||||
|
|||||||
+4
-4
@@ -170,11 +170,11 @@ freqtrade backtesting --strategy AwesomeStrategy --dry-run-wallet 1000
|
|||||||
|
|
||||||
Using a different on-disk historical candle (OHLCV) data source
|
Using a different on-disk historical candle (OHLCV) data source
|
||||||
|
|
||||||
Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory.
|
Assume you downloaded the history data from the Binance exchange and kept it in the `user_data/data/binance-20180101` directory.
|
||||||
You can then use this data for backtesting as follows:
|
You can then use this data for backtesting as follows:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --strategy AwesomeStrategy --datadir user_data/data/bittrex-20180101
|
freqtrade backtesting --strategy AwesomeStrategy --datadir user_data/data/binance-20180101
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -618,13 +618,13 @@ To compare multiple strategies, a list of Strategies can be provided to backtest
|
|||||||
This is limited to 1 timeframe value per run. However, data is only loaded once from disk so if you have multiple
|
This is limited to 1 timeframe value per run. However, data is only loaded once from disk so if you have multiple
|
||||||
strategies you'd like to compare, this will give a nice runtime boost.
|
strategies you'd like to compare, this will give a nice runtime boost.
|
||||||
|
|
||||||
All listed Strategies need to be in the same directory.
|
All listed Strategies need to be in the same directory, unless also `--recursive-strategy-search` is specified, where sub-directories within the strategy directory are also considered.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade backtesting --timerange 20180401-20180410 --timeframe 5m --strategy-list Strategy001 Strategy002 --export trades
|
freqtrade backtesting --timerange 20180401-20180410 --timeframe 5m --strategy-list Strategy001 Strategy002 --export trades
|
||||||
```
|
```
|
||||||
|
|
||||||
This will save the results to `user_data/backtest_results/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
|
This will save the results to `user_data/backtest_results/backtest-result-<datetime>.json`, including results for both `Strategy001` and `Strategy002`.
|
||||||
There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table).
|
There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table).
|
||||||
Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
|
Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
|
||||||
|
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ For example, if you have 10 ETH available in your wallet on the exchange and `tr
|
|||||||
To fully utilize compounding profits when using multiple bots on the same exchange account, you'll want to limit each bot to a certain starting balance.
|
To fully utilize compounding profits when using multiple bots on the same exchange account, you'll want to limit each bot to a certain starting balance.
|
||||||
This can be accomplished by setting `available_capital` to the desired starting balance.
|
This can be accomplished by setting `available_capital` to the desired starting balance.
|
||||||
|
|
||||||
Assuming your account has 10.000 USDT and you want to run 2 different strategies on this exchange.
|
Assuming your account has 10000 USDT and you want to run 2 different strategies on this exchange.
|
||||||
You'd set `available_capital=5000` - granting each bot an initial capital of 5000 USDT.
|
You'd set `available_capital=5000` - granting each bot an initial capital of 5000 USDT.
|
||||||
The bot will then split this starting balance equally into `max_open_trades` buckets.
|
The bot will then split this starting balance equally into `max_open_trades` buckets.
|
||||||
Profitable trades will result in increased stake-sizes for this bot - without affecting the stake-sizes of the other bot.
|
Profitable trades will result in increased stake-sizes for this bot - without affecting the stake-sizes of the other bot.
|
||||||
@@ -594,7 +594,7 @@ creating trades on the exchange.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "binance",
|
||||||
"key": "key",
|
"key": "key",
|
||||||
"secret": "secret",
|
"secret": "secret",
|
||||||
...
|
...
|
||||||
@@ -644,7 +644,7 @@ API Keys are usually only required for live trading (trading for real money, bot
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "binance",
|
||||||
"key": "af8ddd35195e9dc500b9a6f799f6f5c93d89193b",
|
"key": "af8ddd35195e9dc500b9a6f799f6f5c93d89193b",
|
||||||
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
|
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
|
||||||
//"password": "", // Optional, not needed by all exchanges)
|
//"password": "", // Optional, not needed by all exchanges)
|
||||||
|
|||||||
+5
-4
@@ -318,6 +318,7 @@ Additional tests / steps to complete:
|
|||||||
* Check if balance shows correctly (*)
|
* Check if balance shows correctly (*)
|
||||||
* Create market order (*)
|
* Create market order (*)
|
||||||
* Create limit order (*)
|
* Create limit order (*)
|
||||||
|
* Cancel order (*)
|
||||||
* Complete trade (enter + exit) (*)
|
* Complete trade (enter + exit) (*)
|
||||||
* Compare result calculation between exchange and bot
|
* Compare result calculation between exchange and bot
|
||||||
* Ensure fees are applied correctly (check the database against the exchange)
|
* Ensure fees are applied correctly (check the database against the exchange)
|
||||||
@@ -418,6 +419,9 @@ This part of the documentation is aimed at maintainers, and shows how to create
|
|||||||
|
|
||||||
### Create release branch
|
### Create release branch
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Make sure that the `stable` branch is up-to-date!
|
||||||
|
|
||||||
First, pick a commit that's about one week old (to not include latest additions to releases).
|
First, pick a commit that's about one week old (to not include latest additions to releases).
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
@@ -430,14 +434,11 @@ Determine if crucial bugfixes have been made between this commit and the current
|
|||||||
* Merge the release branch (stable) into this branch.
|
* 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 `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.
|
||||||
* Commit this part.
|
* Commit this part.
|
||||||
* push that branch to the remote and create a PR against the stable branch.
|
* 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 `2019.8-dev`.
|
||||||
|
|
||||||
### Create changelog from git commits
|
### Create changelog from git commits
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Make sure that the `stable` branch is up-to-date!
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
# Needs to be done before merging / pulling that branch.
|
# Needs to be done before merging / pulling that branch.
|
||||||
git log --oneline --no-decorate --no-merges stable..new_release
|
git log --oneline --no-decorate --no-merges stable..new_release
|
||||||
|
|||||||
+18
-42
@@ -181,48 +181,6 @@ freqtrade download-data --exchange kraken --dl-trades -p BTC/EUR BCH/EUR
|
|||||||
Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests\sec rate.
|
Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests\sec rate.
|
||||||
So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased.
|
So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased.
|
||||||
|
|
||||||
## Bittrex
|
|
||||||
|
|
||||||
### Order types
|
|
||||||
|
|
||||||
Bittrex does not support market orders. If you have a message at the bot startup about this, you should change order type values set in your configuration and/or in the strategy from `"market"` to `"limit"`. See some more details on this [here in the FAQ](faq.md#im-getting-the-exchange-bittrex-does-not-support-market-orders-message-and-cannot-run-my-strategy).
|
|
||||||
|
|
||||||
Bittrex also does not support `VolumePairlist` due to limited / split API constellation at the moment.
|
|
||||||
Please use `StaticPairlist`. Other pairlists (other than `VolumePairlist`) should not be affected.
|
|
||||||
|
|
||||||
### Volume pairlist
|
|
||||||
|
|
||||||
Bittrex does not support the direct usage of VolumePairList. This can however be worked around by using the advanced mode with `lookback_days: 1` (or more), which will emulate 24h volume.
|
|
||||||
|
|
||||||
Read more in the [pairlist documentation](plugins.md#volumepairlist-advanced-mode).
|
|
||||||
|
|
||||||
### Restricted markets
|
|
||||||
|
|
||||||
Bittrex split its exchange into US and International versions.
|
|
||||||
The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction.
|
|
||||||
|
|
||||||
If you have restricted pairs in your whitelist, you'll get a warning message in the log on Freqtrade startup for each restricted pair.
|
|
||||||
|
|
||||||
The warning message will look similar to the following:
|
|
||||||
|
|
||||||
``` output
|
|
||||||
[...] Message: bittrex {"success":false,"message":"RESTRICTED_MARKET","result":null,"explanation":null}"
|
|
||||||
```
|
|
||||||
|
|
||||||
If you're an "International" customer on the Bittrex exchange, then this warning will probably not impact you.
|
|
||||||
If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your whitelist.
|
|
||||||
|
|
||||||
You can get a list of restricted markets by using the following snippet:
|
|
||||||
|
|
||||||
``` python
|
|
||||||
import ccxt
|
|
||||||
ct = ccxt.bittrex()
|
|
||||||
lm = ct.load_markets()
|
|
||||||
|
|
||||||
res = [p for p, x in lm.items() if 'US' in x['info']['prohibitedIn']]
|
|
||||||
print(res)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Kucoin
|
## Kucoin
|
||||||
|
|
||||||
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||||
@@ -302,6 +260,24 @@ We do strongly recommend to limit all API keys to the IP you're going to use it
|
|||||||
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||||
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
||||||
|
|
||||||
|
## Bitmart
|
||||||
|
|
||||||
|
Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret.
|
||||||
|
It's therefore required to pass the UID as well.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"exchange": {
|
||||||
|
"name": "bitmart",
|
||||||
|
"uid": "your_bitmart_api_key_memo",
|
||||||
|
"secret": "your_exchange_secret",
|
||||||
|
"password": "your_exchange_api_key_password",
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning "Necessary Verification"
|
||||||
|
Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification.
|
||||||
|
|
||||||
## All exchanges
|
## All exchanges
|
||||||
|
|
||||||
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
||||||
|
|||||||
+1
-7
@@ -128,15 +128,9 @@ This warning can point to one of the below problems:
|
|||||||
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
|
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
|
||||||
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
|
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
|
||||||
|
|
||||||
### I'm getting the "RESTRICTED_MARKET" message in the log
|
|
||||||
|
|
||||||
Currently known to happen for US Bittrex users.
|
|
||||||
|
|
||||||
Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information.
|
|
||||||
|
|
||||||
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
|
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
|
||||||
|
|
||||||
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex and Gate.io).
|
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Gate.io).
|
||||||
|
|
||||||
To fix this, redefine order types in the strategy to use "limit" instead of "market":
|
To fix this, redefine order types in the strategy to use "limit" instead of "market":
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Low level feature engineering is performed in the user strategy within a set of
|
|||||||
| Function | Description |
|
| Function | Description |
|
||||||
|---------------|-------------|
|
|---------------|-------------|
|
||||||
| `feature_engineering_expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
| `feature_engineering_expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
||||||
| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
|
| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `indicator_periods_candles`.
|
||||||
| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week).
|
| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week).
|
||||||
| `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
| `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| | **Reinforcement Learning Parameters within the `freqai.rl_config` sub dictionary**
|
| | **Reinforcement Learning Parameters within the `freqai.rl_config` sub dictionary**
|
||||||
| `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model. <br> **Datatype:** Dictionary.
|
| `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model. <br> **Datatype:** Dictionary.
|
||||||
| `train_cycles` | Training time steps will be set based on the `train_cycles * number of training data points. <br> **Datatype:** Integer.
|
| `train_cycles` | Training time steps will be set based on the `train_cycles * number of training data points. <br> **Datatype:** Integer.
|
||||||
| `cpu_count` | Number of processors to dedicate to the Reinforcement Learning training process. <br> **Datatype:** int.
|
|
||||||
| `max_trade_duration_candles`| Guides the agent training to keep trades below desired length. Example usage shown in `prediction_models/ReinforcementLearner.py` within the customizable `calculate_reward()` function. <br> **Datatype:** int.
|
| `max_trade_duration_candles`| Guides the agent training to keep trades below desired length. Example usage shown in `prediction_models/ReinforcementLearner.py` within the customizable `calculate_reward()` function. <br> **Datatype:** int.
|
||||||
| `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentaiton. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website) <br> **Datatype:** string.
|
| `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentaiton. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website) <br> **Datatype:** string.
|
||||||
| `policy_type` | One of the available policy types from stable_baselines3 <br> **Datatype:** string.
|
| `policy_type` | One of the available policy types from stable_baselines3 <br> **Datatype:** string.
|
||||||
|
|||||||
+9
-5
@@ -337,11 +337,15 @@ There are four parameter types each suited for different purposes.
|
|||||||
* `CategoricalParameter` - defines a parameter with a predetermined number of choices.
|
* `CategoricalParameter` - defines a parameter with a predetermined number of choices.
|
||||||
* `BooleanParameter` - Shorthand for `CategoricalParameter([True, False])` - great for "enable" parameters.
|
* `BooleanParameter` - Shorthand for `CategoricalParameter([True, False])` - great for "enable" parameters.
|
||||||
|
|
||||||
!!! Tip "Disabling parameter optimization"
|
### Parameter options
|
||||||
Each parameter takes two boolean parameters:
|
|
||||||
* `load` - when set to `False` it will not load values configured in `buy_params` and `sell_params`.
|
There are two parameter options that can help you to quickly test various ideas:
|
||||||
* `optimize` - when set to `False` parameter will not be included in optimization process.
|
|
||||||
Use these parameters to quickly prototype various ideas.
|
* `optimize` - when set to `False`, the parameter will not be included in optimization process. (Default: True)
|
||||||
|
* `load` - when set to `False`, results of a previous hyperopt run (in `buy_params` and `sell_params` either in your strategy or the JSON output file) will not be used as the starting value for subsequent hyperopts. The default value specified in the parameter will be used instead. (Default: True)
|
||||||
|
|
||||||
|
!!! Tip "Effects of `load=False` on backtesting"
|
||||||
|
Be aware that setting the `load` option to `False` will mean backtesting will also use the default value specified in the parameter and *not* the value found through hyperoptimisation.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case.
|
Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case.
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ For convenience `lookback_days` can be specified, which will imply that 1d candl
|
|||||||
!!! Warning "Performance implications when using lookback range"
|
!!! Warning "Performance implications when using lookback range"
|
||||||
If used in first position in combination with lookback, the computation of the range based volume can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `VolumeFilter` to narrow the pairlist down for further range volume calculation.
|
If used in first position in combination with lookback, the computation of the range based volume can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `VolumeFilter` to narrow the pairlist down for further range volume calculation.
|
||||||
|
|
||||||
??? Tip "Unsupported exchanges (Bittrex, Gemini)"
|
??? Tip "Unsupported exchanges"
|
||||||
On some exchanges (like Bittrex and Gemini), regular VolumePairList does not work as the api does not natively provide 24h volume. This can be worked around by using candle data to build the volume.
|
On some exchanges (like Gemini), regular VolumePairList does not work as the api does not natively provide 24h volume. This can be worked around by using candle data to build the volume.
|
||||||
To roughly simulate 24h volume, you can use the following configuration.
|
To roughly simulate 24h volume, you can use the following configuration.
|
||||||
Please note that These pairlists will only refresh once per day.
|
Please note that These pairlists will only refresh once per day.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ 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.
|
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/)
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bitmart](https://bitmart.com/)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
- [X] [Huobi](http://huobi.com/)
|
- [X] [Huobi](http://huobi.com/)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
markdown==3.5
|
markdown==3.5.1
|
||||||
mkdocs==1.5.3
|
mkdocs==1.5.3
|
||||||
mkdocs-material==9.4.6
|
mkdocs-material==9.5.3
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.3.1
|
pymdown-extensions==10.5
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
|||||||
+6
-3
@@ -134,13 +134,16 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
|||||||
| `reload_config` | Reloads the configuration file.
|
| `reload_config` | Reloads the configuration file.
|
||||||
| `trades` | List last trades. Limited to 500 trades per call.
|
| `trades` | List last trades. Limited to 500 trades per call.
|
||||||
| `trade/<tradeid>` | Get specific trade.
|
| `trade/<tradeid>` | Get specific trade.
|
||||||
| `trade/<tradeid>` | DELETE - Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
| `trades/<tradeid>` | DELETE - Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
||||||
| `trade/<tradeid>/open-order` | DELETE - Cancel open order for this trade.
|
| `trades/<tradeid>/open-order` | DELETE - Cancel open order for this trade.
|
||||||
| `trade/<tradeid>/reload` | GET - Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange.
|
| `trades/<tradeid>/reload` | GET - Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange.
|
||||||
| `show_config` | Shows part of the current configuration with relevant settings to operation.
|
| `show_config` | Shows part of the current configuration with relevant settings to operation.
|
||||||
| `logs` | Shows last log messages.
|
| `logs` | Shows last log messages.
|
||||||
| `status` | Lists all open trades.
|
| `status` | Lists all open trades.
|
||||||
| `count` | Displays number of trades used and available.
|
| `count` | Displays number of trades used and available.
|
||||||
|
| `entries [pair]` | Shows profit statistics for each enter tags for given pair (or all pairs if pair isn't given). Pair is optional.
|
||||||
|
| `exits [pair]` | Shows profit statistics for each exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.
|
||||||
|
| `mix_tags [pair]` | Shows profit statistics for each combinations of enter tag + exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.
|
||||||
| `locks` | Displays currently locked pairs.
|
| `locks` | Displays currently locked pairs.
|
||||||
| `delete_lock <lock_id>` | Deletes (disables) the lock by id.
|
| `delete_lock <lock_id>` | Deletes (disables) the lock by id.
|
||||||
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
|
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
|
||||||
|
|||||||
@@ -489,7 +489,7 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
|
|||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
|
trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
|
||||||
candle = dataframe.iloc[-1].squeeze()
|
candle = dataframe.iloc[-1].squeeze()
|
||||||
sign = 1 if trade.is_short else -1
|
side = 1 if trade.is_short else -1
|
||||||
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2),
|
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2),
|
||||||
current_rate, is_short=trade.is_short,
|
current_rate, is_short=trade.is_short,
|
||||||
leverage=trade.leverage)
|
leverage=trade.leverage)
|
||||||
@@ -760,9 +760,9 @@ The `position_adjustment_enable` strategy property enables the usage of `adjust_
|
|||||||
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
|
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
|
||||||
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions.
|
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions.
|
||||||
|
|
||||||
`max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys.
|
`max_entry_position_adjustment` property is used to limit the number of additional entries per trade (on top of the first entry order) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment entries.
|
||||||
|
|
||||||
The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional buy order should be made (position is increased).
|
The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades).
|
||||||
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
|
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
|
||||||
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
||||||
|
|
||||||
@@ -770,9 +770,11 @@ This callback is **not** called when there is an open order (either buy or sell)
|
|||||||
|
|
||||||
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
||||||
|
|
||||||
Additional Buys are ignored once you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits.
|
Additional entries are ignored once you have reached the maximum amount of extra entries that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits.
|
||||||
|
|
||||||
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. Modifications to leverage are not possible, and the stake-amount is assumed to be before applying leverage.
|
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade.
|
||||||
|
|
||||||
|
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
|
||||||
|
|
||||||
!!! Note "About stake size"
|
!!! Note "About stake size"
|
||||||
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ You can use [recursive-analysis](recursive-analysis.md) to check and find the co
|
|||||||
In this example strategy, this should be set to 400 (`startup_candle_count = 400`), since the minimum needed history for ema100 calculation to make sure the value is correct is 400 candles.
|
In this example strategy, this should be set to 400 (`startup_candle_count = 400`), since the minimum needed history for ema100 calculation to make sure the value is correct is 400 candles.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=400)
|
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||||
```
|
```
|
||||||
|
|
||||||
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
|
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
|
||||||
@@ -486,17 +486,18 @@ for more information.
|
|||||||
|
|
||||||
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||||
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||||
current pair.
|
current pair. Also supports limited pair format strings (see below)
|
||||||
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||||
specified, defaults to:
|
specified, defaults to:
|
||||||
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||||
* {column}_{timeframe} if asset is not specified.
|
* {column}_{timeframe} if asset is not specified.
|
||||||
Format string supports these format variables:
|
Pair format supports these format variables:
|
||||||
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
|
||||||
* {base} - base currency in lower case, for example 'eth'.
|
* {base} - base currency in lower case, for example 'eth'.
|
||||||
* {BASE} - same as {base}, except in upper case.
|
* {BASE} - same as {base}, except in upper case.
|
||||||
* {quote} - quote currency in lower case, for example 'usdt'.
|
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||||
* {QUOTE} - same as {quote}, except in upper case.
|
* {QUOTE} - same as {quote}, except in upper case.
|
||||||
|
Format string additionally supports this variables.
|
||||||
|
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||||
* {column} - name of dataframe column.
|
* {column} - name of dataframe column.
|
||||||
* {timeframe} - timeframe of informative dataframe.
|
* {timeframe} - timeframe of informative dataframe.
|
||||||
:param ffill: ffill dataframe after merging informative pair.
|
:param ffill: ffill dataframe after merging informative pair.
|
||||||
|
|||||||
@@ -570,7 +570,7 @@ def populate_any_indicators(
|
|||||||
```
|
```
|
||||||
|
|
||||||
1. Features - Move to `feature_engineering_expand_all`
|
1. Features - Move to `feature_engineering_expand_all`
|
||||||
2. Basic features, not expanded across `include_periods_candles` - move to`feature_engineering_expand_basic()`.
|
2. Basic features, not expanded across `indicator_periods_candles` - move to`feature_engineering_expand_basic()`.
|
||||||
3. Standard features which should not be expanded - move to `feature_engineering_standard()`.
|
3. Standard features which should not be expanded - move to `feature_engineering_standard()`.
|
||||||
4. Targets - Move this part to `set_freqai_targets()`.
|
4. Targets - Move this part to `set_freqai_targets()`.
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
| `/status` | Lists all open trades
|
| `/status` | Lists all open trades
|
||||||
| `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space.
|
| `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space.
|
||||||
| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
||||||
|
| `/order <trade_id>` | Lists orders of one or more specific trade. Separate multiple <trade_id> with a blank space.
|
||||||
| `/trades [limit]` | List all recently closed trades in a table format.
|
| `/trades [limit]` | List all recently closed trades in a table format.
|
||||||
| `/count` | Displays number of trades used and available
|
| `/count` | Displays number of trades used and available
|
||||||
| `/locks` | Show currently locked pairs.
|
| `/locks` | Show currently locked pairs.
|
||||||
|
|||||||
+30
-26
@@ -242,7 +242,6 @@ bitkk True missing opt: fetchMyTrades
|
|||||||
bitmart True
|
bitmart True
|
||||||
bitmax True missing opt: fetchMyTrades
|
bitmax True missing opt: fetchMyTrades
|
||||||
bitpanda True
|
bitpanda True
|
||||||
bittrex True
|
|
||||||
bitvavo True
|
bitvavo True
|
||||||
bitz True missing opt: fetchMyTrades
|
bitz True missing opt: fetchMyTrades
|
||||||
btcalpha True missing opt: fetchTicker, fetchTickers
|
btcalpha True missing opt: fetchTicker, fetchTickers
|
||||||
@@ -324,7 +323,6 @@ bitpanda True
|
|||||||
bitso False missing: fetchOHLCV
|
bitso False missing: fetchOHLCV
|
||||||
bitstamp True missing opt: fetchTickers
|
bitstamp True missing opt: fetchTickers
|
||||||
bitstamp1 False missing: fetchOrder, fetchOHLCV
|
bitstamp1 False missing: fetchOrder, fetchOHLCV
|
||||||
bittrex True
|
|
||||||
bitvavo True
|
bitvavo True
|
||||||
bitz True missing opt: fetchMyTrades
|
bitz True missing opt: fetchMyTrades
|
||||||
bl3p False missing: fetchOrder, fetchOHLCV
|
bl3p False missing: fetchOrder, fetchOHLCV
|
||||||
@@ -427,25 +425,33 @@ zb True missing opt: fetchMyTrades
|
|||||||
Use the `list-timeframes` subcommand to see the list of timeframes available for the exchange.
|
Use the `list-timeframes` subcommand to see the list of timeframes available for the exchange.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade list-timeframes [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [-1]
|
usage: freqtrade list-timeframes [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
|
[-d PATH] [--userdir PATH]
|
||||||
|
[--exchange EXCHANGE] [-1]
|
||||||
|
|
||||||
optional arguments:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no config is provided.
|
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||||
-1, --one-column Print output in one column.
|
-1, --one-column Print output in one column.
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
--logfile FILE Log to the file specified. Special values are: 'syslog', 'journald'. See the documentation for more details.
|
--logfile FILE, --log-file FILE
|
||||||
|
Log to the file specified. Special values are:
|
||||||
|
'syslog', 'journald'. See the documentation for more
|
||||||
|
details.
|
||||||
-V, --version show program's version number and exit
|
-V, --version show program's version number and exit
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
Specify configuration file (default: `config.json`). Multiple --config options may be used. Can be set to `-`
|
Specify configuration file (default:
|
||||||
to read config from stdin.
|
`userdir/config.json` or `config.json` whichever
|
||||||
-d PATH, --datadir PATH
|
exists). Multiple --config options may be used. Can be
|
||||||
|
set to `-` to read config from stdin.
|
||||||
|
-d PATH, --datadir PATH, --data-dir PATH
|
||||||
Path to directory with historical backtesting data.
|
Path to directory with historical backtesting data.
|
||||||
--userdir PATH, --user-data-dir PATH
|
--userdir PATH, --user-data-dir PATH
|
||||||
Path to userdata directory.
|
Path to userdata directory.
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* Example: see the timeframes for the 'binance' exchange, set in the configuration file:
|
* Example: see the timeframes for the 'binance' exchange, set in the configuration file:
|
||||||
@@ -479,20 +485,17 @@ usage: freqtrade list-markets [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
|
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
|
||||||
[--print-list] [--print-json] [-1] [--print-csv]
|
[--print-list] [--print-json] [-1] [--print-csv]
|
||||||
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
||||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
|
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
||||||
[--trading-mode {spot,margin,futures}]
|
[-a] [--trading-mode {spot,margin,futures}]
|
||||||
|
|
||||||
usage: freqtrade list-pairs [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
usage: freqtrade list-pairs [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
|
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
|
||||||
[--print-list] [--print-json] [-1] [--print-csv]
|
[--print-list] [--print-json] [-1] [--print-csv]
|
||||||
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
||||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
|
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
|
||||||
[--trading-mode {spot,margin,futures}]
|
[--trading-mode {spot,margin,futures}]
|
||||||
|
options:
|
||||||
optional arguments:
|
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||||
config is provided.
|
|
||||||
--print-list Print list of pairs or market symbols. By default data
|
--print-list Print list of pairs or market symbols. By default data
|
||||||
is printed in the tabular format.
|
is printed in the tabular format.
|
||||||
--print-json Print list of pairs or market symbols in JSON format.
|
--print-json Print list of pairs or market symbols in JSON format.
|
||||||
@@ -504,20 +507,22 @@ optional arguments:
|
|||||||
Specify quote currency(-ies). Space-separated list.
|
Specify quote currency(-ies). Space-separated list.
|
||||||
-a, --all Print all pairs or market symbols. By default only
|
-a, --all Print all pairs or market symbols. By default only
|
||||||
active ones are shown.
|
active ones are shown.
|
||||||
--trading-mode {spot,margin,futures}
|
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
|
||||||
Select Trading mode
|
Select Trading mode
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
--logfile FILE Log to the file specified. Special values are:
|
--logfile FILE, --log-file FILE
|
||||||
|
Log to the file specified. Special values are:
|
||||||
'syslog', 'journald'. See the documentation for more
|
'syslog', 'journald'. See the documentation for more
|
||||||
details.
|
details.
|
||||||
-V, --version show program's version number and exit
|
-V, --version show program's version number and exit
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
Specify configuration file (default: `config.json`).
|
Specify configuration file (default:
|
||||||
Multiple --config options may be used. Can be set to
|
`userdir/config.json` or `config.json` whichever
|
||||||
`-` to read config from stdin.
|
exists). Multiple --config options may be used. Can be
|
||||||
-d PATH, --datadir PATH
|
set to `-` to read config from stdin.
|
||||||
|
-d PATH, --datadir PATH, --data-dir PATH
|
||||||
Path to directory with historical backtesting data.
|
Path to directory with historical backtesting data.
|
||||||
--userdir PATH, --user-data-dir PATH
|
--userdir PATH, --user-data-dir PATH
|
||||||
Path to userdata directory.
|
Path to userdata directory.
|
||||||
@@ -532,7 +537,7 @@ Pairs/markets are sorted by its symbol string in the printed output.
|
|||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
* Print the list of active pairs with quote currency USD on exchange, specified in the default
|
* Print the list of active pairs with quote currency USD on exchange, specified in the default
|
||||||
configuration file (i.e. pairs on the "Bittrex" exchange) in JSON format:
|
configuration file (i.e. pairs on the "Binance" exchange) in JSON format:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ freqtrade list-pairs --quote USD --print-json
|
$ freqtrade list-pairs --quote USD --print-json
|
||||||
@@ -564,7 +569,7 @@ usage: freqtrade test-pairlist [-h] [--userdir PATH] [-v] [-c PATH]
|
|||||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
||||||
[-1] [--print-json] [--exchange EXCHANGE]
|
[-1] [--print-json] [--exchange EXCHANGE]
|
||||||
|
|
||||||
optional arguments:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--userdir PATH, --user-data-dir PATH
|
--userdir PATH, --user-data-dir PATH
|
||||||
Path to userdata directory.
|
Path to userdata directory.
|
||||||
@@ -578,8 +583,7 @@ optional arguments:
|
|||||||
Specify quote currency(-ies). Space-separated list.
|
Specify quote currency(-ies). Space-separated list.
|
||||||
-1, --one-column Print output in one column.
|
-1, --one-column Print output in one column.
|
||||||
--print-json Print list of pairs or market symbols in JSON format.
|
--print-json Print list of pairs or market symbols in JSON format.
|
||||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||||
config is provided.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ You can configure this as follows:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The above represents the default (`exit_fill` and `entry_fill` are optional and will default to the above configuration) - modifications are obviously possible.
|
The above represents the default (`exit_fill` and `entry_fill` are optional and will default to the above configuration) - modifications are obviously possible.
|
||||||
|
To disable either of the two default values (`entry_fill` / `exit_fill`), you can assign them an empty array (`exit_fill: []`).
|
||||||
|
|
||||||
Available fields correspond to the fields for webhooks and are documented in the corresponding webhook sections.
|
Available fields correspond to the fields for webhooks and are documented in the corresponding webhook sections.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2023.10'
|
__version__ = '2023.12'
|
||||||
|
|
||||||
if 'dev' in __version__:
|
if 'dev' in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"choices": [
|
"choices": [
|
||||||
"binance",
|
"binance",
|
||||||
"binanceus",
|
"binanceus",
|
||||||
"bittrex",
|
|
||||||
"gate",
|
"gate",
|
||||||
"huobi",
|
"huobi",
|
||||||
"kraken",
|
"kraken",
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def validate_config_schema(conf: Dict[str, Any], preliminary: bool = False) -> D
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_config_consistency(conf: Dict[str, Any], preliminary: bool = False) -> None:
|
def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Validate the configuration consistency.
|
Validate the configuration consistency.
|
||||||
Should be ran after loading both configuration and strategy,
|
Should be ran after loading both configuration and strategy,
|
||||||
@@ -86,7 +86,7 @@ def validate_config_consistency(conf: Dict[str, Any], preliminary: bool = False)
|
|||||||
_validate_ask_orderbook(conf)
|
_validate_ask_orderbook(conf)
|
||||||
_validate_freqai_hyperopt(conf)
|
_validate_freqai_hyperopt(conf)
|
||||||
_validate_freqai_backtest(conf)
|
_validate_freqai_backtest(conf)
|
||||||
_validate_freqai_include_timeframes(conf)
|
_validate_freqai_include_timeframes(conf, preliminary=preliminary)
|
||||||
_validate_consumers(conf)
|
_validate_consumers(conf)
|
||||||
validate_migrated_strategy_settings(conf)
|
validate_migrated_strategy_settings(conf)
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None:
|
|||||||
'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.')
|
'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.')
|
||||||
|
|
||||||
|
|
||||||
def _validate_freqai_include_timeframes(conf: Dict[str, Any]) -> None:
|
def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool) -> None:
|
||||||
freqai_enabled = conf.get('freqai', {}).get('enabled', False)
|
freqai_enabled = conf.get('freqai', {}).get('enabled', False)
|
||||||
if freqai_enabled:
|
if freqai_enabled:
|
||||||
main_tf = conf.get('timeframe', '5m')
|
main_tf = conf.get('timeframe', '5m')
|
||||||
@@ -355,7 +355,7 @@ def _validate_freqai_include_timeframes(conf: Dict[str, Any]) -> None:
|
|||||||
f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}")
|
f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}")
|
||||||
|
|
||||||
# Ensure that the base timeframe is included in the include_timeframes list
|
# Ensure that the base timeframe is included in the include_timeframes list
|
||||||
if main_tf not in freqai_include_timeframes:
|
if not preliminary and main_tf not in freqai_include_timeframes:
|
||||||
feature_parameters = conf.get('freqai', {}).get('feature_parameters', {})
|
feature_parameters = conf.get('freqai', {}).get('feature_parameters', {})
|
||||||
include_timeframes = [main_tf] + freqai_include_timeframes
|
include_timeframes = [main_tf] + freqai_include_timeframes
|
||||||
conf.get('freqai', {}).get('feature_parameters', {}) \
|
conf.get('freqai', {}).get('feature_parameters', {}) \
|
||||||
|
|||||||
@@ -326,7 +326,10 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
|
|||||||
"Please specify a strategy.")
|
"Please specify a strategy.")
|
||||||
|
|
||||||
if strategy not in data['strategy']:
|
if strategy not in data['strategy']:
|
||||||
raise ValueError(f"Strategy {strategy} not available in the backtest result.")
|
raise ValueError(
|
||||||
|
f"Strategy {strategy} not available in the backtest result. "
|
||||||
|
f"Available strategies are '{','.join(data['strategy'].keys())}'"
|
||||||
|
)
|
||||||
|
|
||||||
data = data['strategy'][strategy]['trades']
|
data = data['strategy'][strategy]['trades']
|
||||||
df = pd.DataFrame(data)
|
df = pd.DataFrame(data)
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
|||||||
len_after = len(df)
|
len_after = len(df)
|
||||||
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
|
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
|
||||||
if len_before != len_after:
|
if len_before != len_after:
|
||||||
message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}"
|
message = (f"Missing data fillup for {pair}, {timeframe}: "
|
||||||
f" - {pct_missing:.2%}")
|
f"before: {len_before} - after: {len_after} - {pct_missing:.2%}")
|
||||||
if pct_missing > 0.01:
|
if pct_missing > 0.01:
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -211,8 +211,9 @@ def prepare_results(analysed_trades, stratname,
|
|||||||
timerange=None):
|
timerange=None):
|
||||||
res_df = pd.DataFrame()
|
res_df = pd.DataFrame()
|
||||||
for pair, trades in analysed_trades[stratname].items():
|
for pair, trades in analysed_trades[stratname].items():
|
||||||
trades.dropna(subset=['close_date'], inplace=True)
|
if (trades.shape[0] > 0):
|
||||||
res_df = pd.concat([res_df, trades], ignore_index=True)
|
trades.dropna(subset=['close_date'], inplace=True)
|
||||||
|
res_df = pd.concat([res_df, trades], ignore_index=True)
|
||||||
|
|
||||||
res_df = _select_rows_within_dates(res_df, timerange)
|
res_df = _select_rows_within_dates(res_df, timerange)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from freqtrade.exchange.common import remove_exchange_credentials, MAP_EXCHANGE_
|
|||||||
from freqtrade.exchange.exchange import Exchange
|
from freqtrade.exchange.exchange import Exchange
|
||||||
# isort: on
|
# isort: on
|
||||||
from freqtrade.exchange.binance import Binance
|
from freqtrade.exchange.binance import Binance
|
||||||
|
from freqtrade.exchange.bitmart import Bitmart
|
||||||
from freqtrade.exchange.bitpanda import Bitpanda
|
from freqtrade.exchange.bitpanda import Bitpanda
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
|
||||||
from freqtrade.exchange.bitvavo import Bitvavo
|
from freqtrade.exchange.bitvavo import Bitvavo
|
||||||
from freqtrade.exchange.bybit import Bybit
|
from freqtrade.exchange.bybit import Bybit
|
||||||
from freqtrade.exchange.coinbasepro import Coinbasepro
|
from freqtrade.exchange.coinbasepro import Coinbasepro
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
""" Bitmart exchange subclass """
|
||||||
|
import logging
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Bitmart(Exchange):
|
||||||
|
"""
|
||||||
|
Bitmart exchange class. Contains adjustments needed for Freqtrade to work
|
||||||
|
with this exchange.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ft_has: Dict = {
|
||||||
|
"stoploss_on_exchange": False, # Bitmart API does not support stoploss orders
|
||||||
|
"ohlcv_candle_limit": 200,
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
""" Bittrex exchange subclass """
|
|
||||||
import logging
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from freqtrade.exchange import Exchange
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Bittrex(Exchange):
|
|
||||||
"""
|
|
||||||
Bittrex exchange class. Contains adjustments needed for Freqtrade to work
|
|
||||||
with this exchange.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_ft_has: Dict = {
|
|
||||||
"ohlcv_candle_limit_per_timeframe": {
|
|
||||||
'1m': 1440,
|
|
||||||
'5m': 288,
|
|
||||||
'1h': 744,
|
|
||||||
'1d': 365,
|
|
||||||
},
|
|
||||||
"l2_limit_range": [1, 25, 500],
|
|
||||||
}
|
|
||||||
@@ -29,6 +29,7 @@ class Bybit(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
"ohlcv_has_history": True,
|
"ohlcv_has_history": True,
|
||||||
|
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"ohlcv_has_history": True,
|
"ohlcv_has_history": True,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ MAP_EXCHANGE_CHILDCLASS = {
|
|||||||
|
|
||||||
SUPPORTED_EXCHANGES = [
|
SUPPORTED_EXCHANGES = [
|
||||||
'binance',
|
'binance',
|
||||||
'bittrex',
|
'bitmart',
|
||||||
'gate',
|
'gate',
|
||||||
'huobi',
|
'huobi',
|
||||||
'kraken',
|
'kraken',
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ class Exchange:
|
|||||||
Exchange ohlcv candle limit
|
Exchange ohlcv candle limit
|
||||||
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
|
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
|
||||||
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
|
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
|
||||||
|
TODO: this is most likely no longer needed since only bittrex needed this.
|
||||||
:param timeframe: Timeframe to check
|
:param timeframe: Timeframe to check
|
||||||
:param candle_type: Candle-type
|
:param candle_type: Candle-type
|
||||||
:param since_ms: Starting timestamp
|
:param since_ms: Starting timestamp
|
||||||
@@ -486,11 +487,14 @@ class Exchange:
|
|||||||
except ccxt.BaseError:
|
except ccxt.BaseError:
|
||||||
logger.exception('Unable to initialize markets.')
|
logger.exception('Unable to initialize markets.')
|
||||||
|
|
||||||
def reload_markets(self) -> None:
|
def reload_markets(self, force: bool = False) -> None:
|
||||||
"""Reload markets both sync and async if refresh interval has passed """
|
"""Reload markets both sync and async if refresh interval has passed """
|
||||||
# Check whether markets have to be reloaded
|
# Check whether markets have to be reloaded
|
||||||
if (self._last_markets_refresh > 0) and (
|
if (
|
||||||
self._last_markets_refresh + self.markets_refresh_interval > dt_ts()):
|
not force
|
||||||
|
and self._last_markets_refresh > 0
|
||||||
|
and (self._last_markets_refresh + self.markets_refresh_interval > dt_ts())
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
logger.debug("Performing scheduled market reload..")
|
logger.debug("Performing scheduled market reload..")
|
||||||
try:
|
try:
|
||||||
@@ -1228,16 +1232,16 @@ class Exchange:
|
|||||||
return order
|
return order
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise InsufficientFundsError(
|
raise InsufficientFundsError(
|
||||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to sell amount {amount} at rate {limit_rate}. '
|
f'Tried to {side} amount {amount} at rate {limit_rate} with '
|
||||||
f'Message: {e}') from e
|
f'stop-price {stop_price_norm}. Message: {e}') from e
|
||||||
except ccxt.InvalidOrder as e:
|
except (ccxt.InvalidOrder, ccxt.BadRequest) as e:
|
||||||
# Errors:
|
# Errors:
|
||||||
# `Order would trigger immediately.`
|
# `Order would trigger immediately.`
|
||||||
raise InvalidOrderException(
|
raise InvalidOrderException(
|
||||||
f'Could not create {ordertype} sell order on market {pair}. '
|
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to sell amount {amount} at rate {limit_rate}. '
|
f'Tried to {side} amount {amount} at rate {limit_rate} with '
|
||||||
f'Message: {e}') from e
|
f'stop-price {stop_price_norm}. Message: {e}') from e
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
@@ -1496,8 +1500,9 @@ class Exchange:
|
|||||||
@retrier
|
@retrier
|
||||||
def fetch_bids_asks(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
def fetch_bids_asks(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
||||||
"""
|
"""
|
||||||
|
:param symbols: List of symbols to fetch
|
||||||
:param cached: Allow cached result
|
:param cached: Allow cached result
|
||||||
:return: fetch_tickers result
|
:return: fetch_bids_asks result
|
||||||
"""
|
"""
|
||||||
if not self.exchange_has('fetchBidsAsks'):
|
if not self.exchange_has('fetchBidsAsks'):
|
||||||
return {}
|
return {}
|
||||||
@@ -1546,6 +1551,12 @@ class Exchange:
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Exchange {self._api.name} does not support fetching tickers in batch. '
|
f'Exchange {self._api.name} does not support fetching tickers in batch. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
|
except ccxt.BadSymbol as e:
|
||||||
|
logger.warning(f"Could not load tickers due to {e.__class__.__name__}. Message: {e} ."
|
||||||
|
"Reloading markets.")
|
||||||
|
self.reload_markets(True)
|
||||||
|
# Re-raise exception to repeat the call.
|
||||||
|
raise TemporaryError from e
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
@@ -1954,7 +1965,7 @@ class Exchange:
|
|||||||
|
|
||||||
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
||||||
for res in results:
|
for res in results:
|
||||||
if isinstance(res, Exception):
|
if isinstance(res, BaseException):
|
||||||
logger.warning(f"Async code raised an exception: {repr(res)}")
|
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||||
if raise_:
|
if raise_:
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import psutil
|
import psutil
|
||||||
import rapidjson
|
import rapidjson
|
||||||
from joblib import dump, load
|
|
||||||
from joblib.externals import cloudpickle
|
from joblib.externals import cloudpickle
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@@ -285,6 +284,10 @@ class FreqaiDataDrawer:
|
|||||||
new_pred["date_pred"] = dataframe["date"]
|
new_pred["date_pred"] = dataframe["date"]
|
||||||
hist_preds = self.historic_predictions[pair].copy()
|
hist_preds = self.historic_predictions[pair].copy()
|
||||||
|
|
||||||
|
# ensure both dataframes have the same date format so they can be merged
|
||||||
|
new_pred["date_pred"] = pd.to_datetime(new_pred["date_pred"])
|
||||||
|
hist_preds["date_pred"] = pd.to_datetime(hist_preds["date_pred"])
|
||||||
|
|
||||||
# find the closest common date between new_pred and historic predictions
|
# find the closest common date between new_pred and historic predictions
|
||||||
# and cut off the new_pred dataframe at that date
|
# and cut off the new_pred dataframe at that date
|
||||||
common_dates = pd.merge(new_pred, hist_preds, on="date_pred", how="inner")
|
common_dates = pd.merge(new_pred, hist_preds, on="date_pred", how="inner")
|
||||||
@@ -295,7 +298,9 @@ class FreqaiDataDrawer:
|
|||||||
"predictions. You likely left your FreqAI instance offline "
|
"predictions. You likely left your FreqAI instance offline "
|
||||||
f"for more than {len(dataframe.index)} candles.")
|
f"for more than {len(dataframe.index)} candles.")
|
||||||
|
|
||||||
df_concat = pd.concat([hist_preds, new_pred], ignore_index=True, keys=hist_preds.keys())
|
# reindex new_pred columns to match the historic predictions dataframe
|
||||||
|
new_pred_reindexed = new_pred.reindex(columns=hist_preds.columns)
|
||||||
|
df_concat = pd.concat([hist_preds, new_pred_reindexed], ignore_index=True)
|
||||||
|
|
||||||
# any missing values will get zeroed out so users can see the exact
|
# any missing values will get zeroed out so users can see the exact
|
||||||
# downtime in FreqUI
|
# downtime in FreqUI
|
||||||
@@ -318,9 +323,9 @@ class FreqaiDataDrawer:
|
|||||||
index = self.historic_predictions[pair].index[-1:]
|
index = self.historic_predictions[pair].index[-1:]
|
||||||
columns = self.historic_predictions[pair].columns
|
columns = self.historic_predictions[pair].columns
|
||||||
|
|
||||||
nan_df = pd.DataFrame(np.nan, index=index, columns=columns)
|
zeros_df = pd.DataFrame(np.zeros((1, len(columns))), index=index, columns=columns)
|
||||||
self.historic_predictions[pair] = pd.concat(
|
self.historic_predictions[pair] = pd.concat(
|
||||||
[self.historic_predictions[pair], nan_df], ignore_index=True, axis=0)
|
[self.historic_predictions[pair], zeros_df], ignore_index=True, axis=0)
|
||||||
df = self.historic_predictions[pair]
|
df = self.historic_predictions[pair]
|
||||||
|
|
||||||
# model outputs and associated statistics
|
# model outputs and associated statistics
|
||||||
@@ -471,7 +476,8 @@ class FreqaiDataDrawer:
|
|||||||
|
|
||||||
# Save the trained model
|
# Save the trained model
|
||||||
if self.model_type == 'joblib':
|
if self.model_type == 'joblib':
|
||||||
dump(model, save_path / f"{dk.model_filename}_model.joblib")
|
with (save_path / f"{dk.model_filename}_model.joblib").open("wb") as fp:
|
||||||
|
cloudpickle.dump(model, fp)
|
||||||
elif self.model_type == 'keras':
|
elif self.model_type == 'keras':
|
||||||
model.save(save_path / f"{dk.model_filename}_model.h5")
|
model.save(save_path / f"{dk.model_filename}_model.h5")
|
||||||
elif self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]:
|
elif self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]:
|
||||||
@@ -558,7 +564,8 @@ class FreqaiDataDrawer:
|
|||||||
if dk.live and coin in self.model_dictionary:
|
if dk.live and coin in self.model_dictionary:
|
||||||
model = self.model_dictionary[coin]
|
model = self.model_dictionary[coin]
|
||||||
elif self.model_type == 'joblib':
|
elif self.model_type == 'joblib':
|
||||||
model = load(dk.data_path / f"{dk.model_filename}_model.joblib")
|
with (dk.data_path / f"{dk.model_filename}_model.joblib").open("rb") as fp:
|
||||||
|
model = cloudpickle.load(fp)
|
||||||
elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type:
|
elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type:
|
||||||
mod = importlib.import_module(
|
mod = importlib.import_module(
|
||||||
self.model_type, self.freqai_info['rl_config']['model_type'])
|
self.model_type, self.freqai_info['rl_config']['model_type'])
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ class FreqaiDataKitchen:
|
|||||||
f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points"
|
f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points"
|
||||||
f" due to NaNs in populated dataset {len(unfiltered_df)}."
|
f" due to NaNs in populated dataset {len(unfiltered_df)}."
|
||||||
)
|
)
|
||||||
if len(unfiltered_df) == 0 and not self.live:
|
if len(filtered_df) == 0 and not self.live:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"{self.pair}: all training data dropped due to NaNs. "
|
f"{self.pair}: all training data dropped due to NaNs. "
|
||||||
"You likely did not download enough training data prior "
|
"You likely did not download enough training data prior "
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor):
|
|||||||
...
|
...
|
||||||
"freqai": {
|
"freqai": {
|
||||||
...
|
...
|
||||||
|
"conv_width": 30, // PyTorchTransformer is based on windowing
|
||||||
|
"feature_parameters": {
|
||||||
|
...
|
||||||
|
"include_shifted_candles": 0, // which removes the need for shifted candles
|
||||||
|
...
|
||||||
|
},
|
||||||
"model_training_parameters" : {
|
"model_training_parameters" : {
|
||||||
"learning_rate": 3e-4,
|
"learning_rate": 3e-4,
|
||||||
"trainer_kwargs": {
|
"trainer_kwargs": {
|
||||||
@@ -120,16 +126,16 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor):
|
|||||||
# create empty torch tensor
|
# create empty torch tensor
|
||||||
self.model.model.eval()
|
self.model.model.eval()
|
||||||
yb = torch.empty(0).to(self.device)
|
yb = torch.empty(0).to(self.device)
|
||||||
if x.shape[1] > 1:
|
if x.shape[1] > self.window_size:
|
||||||
ws = self.window_size
|
ws = self.window_size
|
||||||
for i in range(0, x.shape[1] - ws):
|
for i in range(0, x.shape[1] - ws):
|
||||||
xb = x[:, i:i + ws, :].to(self.device)
|
xb = x[:, i:i + ws, :].to(self.device)
|
||||||
y = self.model.model(xb)
|
y = self.model.model(xb)
|
||||||
yb = torch.cat((yb, y), dim=0)
|
yb = torch.cat((yb, y), dim=1)
|
||||||
else:
|
else:
|
||||||
yb = self.model.model(x)
|
yb = self.model.model(x)
|
||||||
|
|
||||||
yb = yb.cpu().squeeze()
|
yb = yb.cpu().squeeze(0)
|
||||||
pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list)
|
pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list)
|
||||||
pred_df, _, _ = dk.label_pipeline.inverse_transform(pred_df)
|
pred_df, _, _ = dk.label_pipeline.inverse_transform(pred_df)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
from pandas import DataFrame
|
||||||
|
from sklearn.ensemble import RandomForestClassifier
|
||||||
|
from sklearn.preprocessing import LabelEncoder
|
||||||
|
|
||||||
|
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
|
||||||
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SKLearnRandomForestClassifier(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
|
||||||
|
"""
|
||||||
|
|
||||||
|
X = data_dictionary["train_features"].to_numpy()
|
||||||
|
y = data_dictionary["train_labels"].to_numpy()[:, 0]
|
||||||
|
|
||||||
|
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0:
|
||||||
|
eval_set = None
|
||||||
|
else:
|
||||||
|
test_features = data_dictionary["test_features"].to_numpy()
|
||||||
|
test_labels = data_dictionary["test_labels"].to_numpy()[:, 0]
|
||||||
|
|
||||||
|
eval_set = (test_features, test_labels)
|
||||||
|
|
||||||
|
if self.freqai_info.get("continual_learning", False):
|
||||||
|
logger.warning("Continual learning is not supported for "
|
||||||
|
"SKLearnRandomForestClassifier, ignoring.")
|
||||||
|
|
||||||
|
train_weights = data_dictionary["train_weights"]
|
||||||
|
|
||||||
|
model = RandomForestClassifier(**self.model_training_parameters)
|
||||||
|
|
||||||
|
model.fit(X=X, y=y, sample_weight=train_weights)
|
||||||
|
if eval_set:
|
||||||
|
logger.info("Score: %s", model.score(eval_set[0], eval_set[1]))
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
def predict(
|
||||||
|
self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs
|
||||||
|
) -> Tuple[DataFrame, npt.NDArray[np.int_]]:
|
||||||
|
"""
|
||||||
|
Filter the prediction features data and predict with it.
|
||||||
|
:param unfiltered_df: Full dataframe for the current backtest period.
|
||||||
|
:return:
|
||||||
|
:pred_df: dataframe containing the predictions
|
||||||
|
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
|
||||||
|
data (NaNs) or felt uncertain about data (PCA and DI index)
|
||||||
|
"""
|
||||||
|
|
||||||
|
(pred_df, dk.do_predict) = super().predict(unfiltered_df, dk, **kwargs)
|
||||||
|
|
||||||
|
le = LabelEncoder()
|
||||||
|
label = dk.label_list[0]
|
||||||
|
labels_before = list(dk.data['labels_std'].keys())
|
||||||
|
labels_after = le.fit_transform(labels_before).tolist()
|
||||||
|
pred_df[label] = le.inverse_transform(pred_df[label])
|
||||||
|
pred_df = pred_df.rename(
|
||||||
|
columns={labels_after[i]: labels_before[i] for i in range(len(labels_before))})
|
||||||
|
|
||||||
|
return (pred_df, dk.do_predict)
|
||||||
@@ -45,7 +45,7 @@ class XGBoostRFRegressor(BaseRegressionModel):
|
|||||||
|
|
||||||
model = XGBRFRegressor(**self.model_training_parameters)
|
model = XGBRFRegressor(**self.model_training_parameters)
|
||||||
|
|
||||||
model.set_params(callbacks=[TBCallback(dk.data_path)], activate=self.activate_tensorboard)
|
model.set_params(callbacks=[TBCallback(dk.data_path)])
|
||||||
model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set,
|
model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set,
|
||||||
sample_weight_eval_set=eval_weights, xgb_model=xgb_model)
|
sample_weight_eval_set=eval_weights, xgb_model=xgb_model)
|
||||||
# set the callbacks to empty so that we can serialize to disk later
|
# set the callbacks to empty so that we can serialize to disk later
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class XGBoostRegressor(BaseRegressionModel):
|
|||||||
|
|
||||||
model = XGBRegressor(**self.model_training_parameters)
|
model = XGBRegressor(**self.model_training_parameters)
|
||||||
|
|
||||||
model.set_params(callbacks=[TBCallback(dk.data_path)], activate=self.activate_tensorboard)
|
model.set_params(callbacks=[TBCallback(dk.data_path)])
|
||||||
model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set,
|
model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set,
|
||||||
sample_weight_eval_set=eval_weights, xgb_model=xgb_model)
|
sample_weight_eval_set=eval_weights, xgb_model=xgb_model)
|
||||||
# set the callbacks to empty so that we can serialize to disk later
|
# set the callbacks to empty so that we can serialize to disk later
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from typing import Any, Dict, Type, Union
|
|||||||
|
|
||||||
from stable_baselines3.common.callbacks import BaseCallback
|
from stable_baselines3.common.callbacks import BaseCallback
|
||||||
from stable_baselines3.common.logger import HParam
|
from stable_baselines3.common.logger import HParam
|
||||||
from stable_baselines3.common.vec_env import VecEnv
|
|
||||||
|
|
||||||
from freqtrade.freqai.RL.BaseEnvironment import BaseActions
|
from freqtrade.freqai.RL.BaseEnvironment import BaseActions
|
||||||
|
|
||||||
@@ -13,13 +12,9 @@ class TensorboardCallback(BaseCallback):
|
|||||||
Custom callback for plotting additional values in tensorboard and
|
Custom callback for plotting additional values in tensorboard and
|
||||||
episodic summary reports.
|
episodic summary reports.
|
||||||
"""
|
"""
|
||||||
# Override training_env type to fix type errors
|
|
||||||
training_env: Union[VecEnv, None] = None
|
|
||||||
|
|
||||||
def __init__(self, verbose=1, actions: Type[Enum] = BaseActions):
|
def __init__(self, verbose=1, actions: Type[Enum] = BaseActions):
|
||||||
super().__init__(verbose)
|
super().__init__(verbose)
|
||||||
self.model: Any = None
|
self.model: Any = None
|
||||||
self.logger: Any = None
|
|
||||||
self.actions: Type[Enum] = actions
|
self.actions: Type[Enum] = actions
|
||||||
|
|
||||||
def _on_training_start(self) -> None:
|
def _on_training_start(self) -> None:
|
||||||
@@ -47,8 +42,6 @@ class TensorboardCallback(BaseCallback):
|
|||||||
def _on_step(self) -> bool:
|
def _on_step(self) -> bool:
|
||||||
|
|
||||||
local_info = self.locals["infos"][0]
|
local_info = self.locals["infos"][0]
|
||||||
if self.training_env is None:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if hasattr(self.training_env, 'envs'):
|
if hasattr(self.training_env, 'envs'):
|
||||||
tensorboard_metrics = self.training_env.envs[0].unwrapped.tensorboard_metrics
|
tensorboard_metrics = self.training_env.envs[0].unwrapped.tensorboard_metrics
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
|
|||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.rpc import RPCManager
|
from freqtrade.rpc import RPCManager
|
||||||
from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
|
from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
|
||||||
from freqtrade.rpc.rpc_types import (RPCBuyMsg, RPCCancelMsg, RPCProtectionMsg, RPCSellCancelMsg,
|
from freqtrade.rpc.rpc_types import (ProfitLossStr, RPCCancelMsg, RPCEntryMsg, RPCExitCancelMsg,
|
||||||
RPCSellMsg)
|
RPCExitMsg, RPCProtectionMsg)
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from freqtrade.util import FtPrecise
|
from freqtrade.util import FtPrecise
|
||||||
@@ -580,7 +580,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
else:
|
else:
|
||||||
self.log_once(f"Pair {pair} is currently locked.", logger.info)
|
self.log_once(f"Pair {pair} is currently locked.", logger.info)
|
||||||
return False
|
return False
|
||||||
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
|
stake_amount = self.wallets.get_trade_stake_amount(
|
||||||
|
pair, self.config['max_open_trades'], self.edge)
|
||||||
|
|
||||||
bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {})
|
bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {})
|
||||||
if ((bid_check_dom.get('enabled', False)) and
|
if ((bid_check_dom.get('enabled', False)) and
|
||||||
@@ -903,7 +904,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# First cancelling stoploss on exchange ...
|
# First cancelling stoploss on exchange ...
|
||||||
if trade.stoploss_order_id:
|
if trade.stoploss_order_id:
|
||||||
try:
|
try:
|
||||||
logger.info(f"Canceling stoploss on exchange for {trade}")
|
logger.info(f"Cancelling stoploss on exchange for {trade}")
|
||||||
co = self.exchange.cancel_stoploss_order_with_result(
|
co = self.exchange.cancel_stoploss_order_with_result(
|
||||||
trade.stoploss_order_id, trade.pair, trade.amount)
|
trade.stoploss_order_id, trade.pair, trade.amount)
|
||||||
self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True)
|
self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True)
|
||||||
@@ -1014,7 +1015,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
current_rate = self.exchange.get_rate(
|
current_rate = self.exchange.get_rate(
|
||||||
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
|
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
|
||||||
|
|
||||||
msg: RPCBuyMsg = {
|
msg: RPCEntryMsg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY,
|
'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY,
|
||||||
'buy_tag': trade.enter_tag,
|
'buy_tag': trade.enter_tag,
|
||||||
@@ -1791,9 +1792,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order_rate = trade.safe_close_rate
|
order_rate = trade.safe_close_rate
|
||||||
profit = trade.calculate_profit(rate=order_rate)
|
profit = trade.calculate_profit(rate=order_rate)
|
||||||
amount = trade.amount
|
amount = trade.amount
|
||||||
gain = "profit" if profit.profit_ratio > 0 else "loss"
|
gain: ProfitLossStr = "profit" if profit.profit_ratio > 0 else "loss"
|
||||||
|
|
||||||
msg: RPCSellMsg = {
|
msg: RPCExitMsg = {
|
||||||
'type': (RPCMessageType.EXIT_FILL if fill
|
'type': (RPCMessageType.EXIT_FILL if fill
|
||||||
else RPCMessageType.EXIT),
|
else RPCMessageType.EXIT),
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
@@ -1845,9 +1846,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
profit = trade.calculate_profit(rate=profit_rate)
|
profit = trade.calculate_profit(rate=profit_rate)
|
||||||
current_rate = self.exchange.get_rate(
|
current_rate = self.exchange.get_rate(
|
||||||
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
|
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
|
||||||
gain = "profit" if profit.profit_ratio > 0 else "loss"
|
gain: ProfitLossStr = "profit" if profit.profit_ratio > 0 else "loss"
|
||||||
|
|
||||||
msg: RPCSellCancelMsg = {
|
msg: RPCExitCancelMsg = {
|
||||||
'type': RPCMessageType.EXIT_CANCEL,
|
'type': RPCMessageType.EXIT_CANCEL,
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'exchange': trade.exchange.capitalize(),
|
'exchange': trade.exchange.capitalize(),
|
||||||
|
|||||||
+2
-1
@@ -3,6 +3,7 @@ Various tool function for Freqtrade and scripts
|
|||||||
"""
|
"""
|
||||||
import gzip
|
import gzip
|
||||||
import logging
|
import logging
|
||||||
|
from io import StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterator, List, Mapping, Optional, TextIO, Union
|
from typing import Any, Dict, Iterator, List, Mapping, Optional, TextIO, Union
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@@ -231,7 +232,7 @@ def json_to_dataframe(data: str) -> pd.DataFrame:
|
|||||||
:param data: A JSON string
|
:param data: A JSON string
|
||||||
:returns: A pandas DataFrame from the JSON string
|
:returns: A pandas DataFrame from the JSON string
|
||||||
"""
|
"""
|
||||||
dataframe = pd.read_json(data, orient='split')
|
dataframe = pd.read_json(StringIO(data), orient='split')
|
||||||
if 'date' in dataframe.columns:
|
if 'date' in dataframe.columns:
|
||||||
dataframe['date'] = pd.to_datetime(dataframe['date'], unit='ms', utc=True)
|
dataframe['date'] = pd.to_datetime(dataframe['date'], unit='ms', utc=True)
|
||||||
|
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ class LookaheadAnalysis(BaseAnalysis):
|
|||||||
# compare_df now comprises tuples with [1] having either 'self' or 'other'
|
# compare_df now comprises tuples with [1] having either 'self' or 'other'
|
||||||
if 'other' in col_name[1]:
|
if 'other' in col_name[1]:
|
||||||
continue
|
continue
|
||||||
self_value = compare_df_row[col_idx]
|
self_value = compare_df_row.iloc[col_idx]
|
||||||
other_value = compare_df_row[col_idx + 1]
|
other_value = compare_df_row.iloc[col_idx + 1]
|
||||||
|
|
||||||
# output differences
|
# output differences
|
||||||
if self_value != other_value:
|
if self_value != other_value:
|
||||||
|
|||||||
@@ -276,11 +276,13 @@ class Backtesting:
|
|||||||
else:
|
else:
|
||||||
self.detail_data = {}
|
self.detail_data = {}
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
self.funding_fee_timeframe: str = self.exchange.get_option('mark_ohlcv_timeframe')
|
||||||
|
self.funding_fee_timeframe_secs: int = timeframe_to_seconds(self.funding_fee_timeframe)
|
||||||
# Load additional futures data.
|
# Load additional futures data.
|
||||||
funding_rates_dict = history.load_data(
|
funding_rates_dict = history.load_data(
|
||||||
datadir=self.config['datadir'],
|
datadir=self.config['datadir'],
|
||||||
pairs=self.pairlists.whitelist,
|
pairs=self.pairlists.whitelist,
|
||||||
timeframe=self.exchange.get_option('mark_ohlcv_timeframe'),
|
timeframe=self.funding_fee_timeframe,
|
||||||
timerange=self.timerange,
|
timerange=self.timerange,
|
||||||
startup_candles=0,
|
startup_candles=0,
|
||||||
fail_without_data=True,
|
fail_without_data=True,
|
||||||
@@ -292,7 +294,7 @@ class Backtesting:
|
|||||||
mark_rates_dict = history.load_data(
|
mark_rates_dict = history.load_data(
|
||||||
datadir=self.config['datadir'],
|
datadir=self.config['datadir'],
|
||||||
pairs=self.pairlists.whitelist,
|
pairs=self.pairlists.whitelist,
|
||||||
timeframe=self.exchange.get_option('mark_ohlcv_timeframe'),
|
timeframe=self.funding_fee_timeframe,
|
||||||
timerange=self.timerange,
|
timerange=self.timerange,
|
||||||
startup_candles=0,
|
startup_candles=0,
|
||||||
fail_without_data=True,
|
fail_without_data=True,
|
||||||
@@ -597,6 +599,8 @@ class Backtesting:
|
|||||||
"""
|
"""
|
||||||
if order and self._get_order_filled(order.ft_price, row):
|
if order and self._get_order_filled(order.ft_price, row):
|
||||||
order.close_bt_order(current_date, trade)
|
order.close_bt_order(current_date, trade)
|
||||||
|
self._run_funding_fees(trade, current_date, force=True)
|
||||||
|
|
||||||
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount):
|
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount):
|
||||||
# trade is still open
|
# trade is still open
|
||||||
trade.set_liquidation_price(self.exchange.get_liquidation_price(
|
trade.set_liquidation_price(self.exchange.get_liquidation_price(
|
||||||
@@ -718,16 +722,7 @@ class Backtesting:
|
|||||||
self, trade: LocalTrade, row: Tuple, current_time: datetime
|
self, trade: LocalTrade, row: Tuple, current_time: datetime
|
||||||
) -> Optional[LocalTrade]:
|
) -> Optional[LocalTrade]:
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
self._run_funding_fees(trade, current_time)
|
||||||
trade.set_funding_fees(
|
|
||||||
self.exchange.calculate_funding_fees(
|
|
||||||
self.futures_data[trade.pair],
|
|
||||||
amount=trade.amount,
|
|
||||||
is_short=trade.is_short,
|
|
||||||
open_date=trade.date_last_filled_utc,
|
|
||||||
close_date=current_time
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if we need to adjust our current positions
|
# Check if we need to adjust our current positions
|
||||||
if self.strategy.position_adjustment_enable:
|
if self.strategy.position_adjustment_enable:
|
||||||
@@ -746,6 +741,27 @@ class Backtesting:
|
|||||||
return t
|
return t
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _run_funding_fees(self, trade: LocalTrade, current_time: datetime, force: bool = False):
|
||||||
|
"""
|
||||||
|
Calculate funding fees if necessary and add them to the trade.
|
||||||
|
"""
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
|
||||||
|
if (
|
||||||
|
force
|
||||||
|
or (current_time.timestamp() % self.funding_fee_timeframe_secs) == 0
|
||||||
|
):
|
||||||
|
# Funding fee interval.
|
||||||
|
trade.set_funding_fees(
|
||||||
|
self.exchange.calculate_funding_fees(
|
||||||
|
self.futures_data[trade.pair],
|
||||||
|
amount=trade.amount,
|
||||||
|
is_short=trade.is_short,
|
||||||
|
open_date=trade.date_last_filled_utc,
|
||||||
|
close_date=current_time
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def get_valid_price_and_stake(
|
def get_valid_price_and_stake(
|
||||||
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
|
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
|
||||||
direction: LongShort, current_time: datetime, entry_tag: Optional[str],
|
direction: LongShort, current_time: datetime, entry_tag: Optional[str],
|
||||||
@@ -775,7 +791,8 @@ class Backtesting:
|
|||||||
leverage = trade.leverage if trade else 1.0
|
leverage = trade.leverage if trade else 1.0
|
||||||
if not pos_adjust:
|
if not pos_adjust:
|
||||||
try:
|
try:
|
||||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False)
|
stake_amount = self.wallets.get_trade_stake_amount(
|
||||||
|
pair, self.strategy.max_open_trades, update=False)
|
||||||
except DependencyException:
|
except DependencyException:
|
||||||
return 0, 0, 0, 0
|
return 0, 0, 0, 0
|
||||||
|
|
||||||
@@ -957,7 +974,7 @@ class Backtesting:
|
|||||||
|
|
||||||
def trade_slot_available(self, open_trade_count: int) -> bool:
|
def trade_slot_available(self, open_trade_count: int) -> bool:
|
||||||
# Always allow trades when max_open_trades is enabled.
|
# Always allow trades when max_open_trades is enabled.
|
||||||
max_open_trades: IntOrInf = self.config['max_open_trades']
|
max_open_trades: IntOrInf = self.strategy.max_open_trades
|
||||||
if max_open_trades <= 0 or open_trade_count < max_open_trades:
|
if max_open_trades <= 0 or open_trade_count < max_open_trades:
|
||||||
return True
|
return True
|
||||||
# Rejected trade
|
# Rejected trade
|
||||||
|
|||||||
@@ -500,7 +500,7 @@ class Hyperopt:
|
|||||||
while i < 5 and len(asked_non_tried) < n_points:
|
while i < 5 and len(asked_non_tried) < n_points:
|
||||||
if i < 3:
|
if i < 3:
|
||||||
self.opt.cache_ = {}
|
self.opt.cache_ = {}
|
||||||
asked = unique_list(self.opt.ask(n_points=n_points * 5))
|
asked = unique_list(self.opt.ask(n_points=n_points * 5 if i > 0 else n_points))
|
||||||
is_random = [False for _ in range(len(asked))]
|
is_random = [False for _ in range(len(asked))]
|
||||||
else:
|
else:
|
||||||
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
|
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
|
||||||
@@ -637,6 +637,10 @@ class Hyperopt:
|
|||||||
|
|
||||||
HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs,
|
HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs,
|
||||||
self.print_json)
|
self.print_json)
|
||||||
|
elif self.num_epochs_saved > 0:
|
||||||
|
print(
|
||||||
|
f"No good result found for given optimization function in {self.num_epochs_saved} "
|
||||||
|
f"{plural(self.num_epochs_saved, 'epoch')}.")
|
||||||
else:
|
else:
|
||||||
# This is printed when Ctrl+C is pressed quickly, before first epochs have
|
# This is printed when Ctrl+C is pressed quickly, before first epochs have
|
||||||
# a chance to be evaluated.
|
# a chance to be evaluated.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def _format_exception_message(space: str, ignore_missing_space: bool) -> None:
|
def _format_exception_message(space: str, ignore_missing_space: bool) -> None:
|
||||||
msg = (f"The '{space}' space is included into the hyperoptimization "
|
msg = (f"The '{space}' space is included into the hyperoptimization "
|
||||||
f"but no parameter for this space was not found in your Strategy. "
|
f"but no parameter for this space was found in your Strategy. "
|
||||||
)
|
)
|
||||||
if ignore_missing_space:
|
if ignore_missing_space:
|
||||||
logger.warning(msg + "This space will be ignored.")
|
logger.warning(msg + "This space will be ignored.")
|
||||||
|
|||||||
@@ -429,14 +429,18 @@ class HyperoptTools:
|
|||||||
trials = trials.drop(columns=['Total profit'])
|
trials = trials.drop(columns=['Total profit'])
|
||||||
|
|
||||||
if print_colorized:
|
if print_colorized:
|
||||||
|
trials2 = trials.astype(str)
|
||||||
for i in range(len(trials)):
|
for i in range(len(trials)):
|
||||||
if trials.loc[i]['is_profit']:
|
if trials.loc[i]['is_profit']:
|
||||||
for j in range(len(trials.loc[i]) - 3):
|
for j in range(len(trials.loc[i]) - 3):
|
||||||
trials.iat[i, j] = f"{Fore.GREEN}{str(trials.loc[i][j])}{Fore.RESET}"
|
trials2.iat[i, j] = f"{Fore.GREEN}{str(trials.iloc[i, j])}{Fore.RESET}"
|
||||||
if trials.loc[i]['is_best'] and highlight_best:
|
if trials.loc[i]['is_best'] and highlight_best:
|
||||||
for j in range(len(trials.loc[i]) - 3):
|
for j in range(len(trials.loc[i]) - 3):
|
||||||
trials.iat[i, j] = f"{Style.BRIGHT}{str(trials.loc[i][j])}{Style.RESET_ALL}"
|
trials2.iat[i, j] = (
|
||||||
|
f"{Style.BRIGHT}{str(trials.iloc[i, j])}{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
trials = trials2
|
||||||
|
del trials2
|
||||||
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit', 'is_random'])
|
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit', 'is_random'])
|
||||||
if remove_header > 0:
|
if remove_header > 0:
|
||||||
table = tabulate.tabulate(
|
table = tabulate.tabulate(
|
||||||
|
|||||||
@@ -219,8 +219,10 @@ def _get_resample_from_period(period: str) -> str:
|
|||||||
raise ValueError(f"Period {period} is not supported.")
|
raise ValueError(f"Period {period} is not supported.")
|
||||||
|
|
||||||
|
|
||||||
def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dict[str, Any]]:
|
def generate_periodic_breakdown_stats(
|
||||||
results = DataFrame.from_records(trade_list)
|
trade_list: Union[List, DataFrame], period: str) -> List[Dict[str, Any]]:
|
||||||
|
|
||||||
|
results = trade_list if not isinstance(trade_list, list) else DataFrame.from_records(trade_list)
|
||||||
if len(results) == 0:
|
if len(results) == 0:
|
||||||
return []
|
return []
|
||||||
results['close_date'] = to_datetime(results['close_date'], utc=True)
|
results['close_date'] = to_datetime(results['close_date'], utc=True)
|
||||||
|
|||||||
@@ -156,20 +156,20 @@ class Order(ModelBase):
|
|||||||
if self.order_id != str(order['id']):
|
if self.order_id != str(order['id']):
|
||||||
raise DependencyException("Order-id's don't match")
|
raise DependencyException("Order-id's don't match")
|
||||||
|
|
||||||
self.status = order.get('status', self.status)
|
self.status = safe_value_fallback(order, 'status', default_value=self.status)
|
||||||
self.symbol = order.get('symbol', self.symbol)
|
self.symbol = safe_value_fallback(order, 'symbol', default_value=self.symbol)
|
||||||
self.order_type = order.get('type', self.order_type)
|
self.order_type = safe_value_fallback(order, 'type', default_value=self.order_type)
|
||||||
self.side = order.get('side', self.side)
|
self.side = safe_value_fallback(order, 'side', default_value=self.side)
|
||||||
self.price = order.get('price', self.price)
|
self.price = safe_value_fallback(order, 'price', default_value=self.price)
|
||||||
self.amount = order.get('amount', self.amount)
|
self.amount = safe_value_fallback(order, 'amount', default_value=self.amount)
|
||||||
self.filled = order.get('filled', self.filled)
|
self.filled = safe_value_fallback(order, 'filled', default_value=self.filled)
|
||||||
self.average = order.get('average', self.average)
|
self.average = safe_value_fallback(order, 'average', default_value=self.average)
|
||||||
self.remaining = order.get('remaining', self.remaining)
|
self.remaining = safe_value_fallback(order, 'remaining', default_value=self.remaining)
|
||||||
self.cost = order.get('cost', self.cost)
|
self.cost = safe_value_fallback(order, 'cost', default_value=self.cost)
|
||||||
self.stop_price = order.get('stopPrice', self.stop_price)
|
self.stop_price = safe_value_fallback(order, 'stopPrice', default_value=self.stop_price)
|
||||||
|
order_date = safe_value_fallback(order, 'timestamp')
|
||||||
if 'timestamp' in order and order['timestamp'] is not None:
|
if order_date:
|
||||||
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
|
self.order_date = datetime.fromtimestamp(order_date / 1000, tz=timezone.utc)
|
||||||
|
|
||||||
self.ft_is_open = True
|
self.ft_is_open = True
|
||||||
if self.status in NON_OPEN_EXCHANGE_STATES:
|
if self.status in NON_OPEN_EXCHANGE_STATES:
|
||||||
@@ -1053,7 +1053,7 @@ class LocalTrade:
|
|||||||
price = avg_price if is_exit else tmp_price
|
price = avg_price if is_exit else tmp_price
|
||||||
current_stake += price * tmp_amount * side
|
current_stake += price * tmp_amount * side
|
||||||
|
|
||||||
if current_amount > ZERO:
|
if current_amount > ZERO and not is_exit:
|
||||||
avg_price = current_stake / current_amount
|
avg_price = current_stake / current_amount
|
||||||
|
|
||||||
if is_exit:
|
if is_exit:
|
||||||
@@ -1066,7 +1066,10 @@ class LocalTrade:
|
|||||||
exit_amount = o.safe_amount_after_fee
|
exit_amount = o.safe_amount_after_fee
|
||||||
prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price))
|
prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price))
|
||||||
close_profit_abs += prof.profit_abs
|
close_profit_abs += prof.profit_abs
|
||||||
close_profit = prof.profit_ratio
|
if total_stake > 0:
|
||||||
|
# This needs to be calculated based on the last occuring exit to be aligned
|
||||||
|
# with realized_profit.
|
||||||
|
close_profit = (close_profit_abs / total_stake) * self.leverage
|
||||||
else:
|
else:
|
||||||
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
|
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
|
||||||
max_stake_amount += (tmp_amount * price)
|
max_stake_amount += (tmp_amount * price)
|
||||||
@@ -1780,7 +1783,7 @@ class Trade(ModelBase, LocalTrade):
|
|||||||
.order_by(desc('profit_sum_abs'))
|
.order_by(desc('profit_sum_abs'))
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
return_list: List[Dict] = []
|
resp: List[Dict] = []
|
||||||
for id, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf:
|
for id, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf:
|
||||||
enter_tag = enter_tag if enter_tag is not None else "Other"
|
enter_tag = enter_tag if enter_tag is not None else "Other"
|
||||||
exit_reason = exit_reason if exit_reason is not None else "Other"
|
exit_reason = exit_reason if exit_reason is not None else "Other"
|
||||||
@@ -1788,24 +1791,25 @@ class Trade(ModelBase, LocalTrade):
|
|||||||
if (exit_reason is not None and enter_tag is not None):
|
if (exit_reason is not None and enter_tag is not None):
|
||||||
mix_tag = enter_tag + " " + exit_reason
|
mix_tag = enter_tag + " " + exit_reason
|
||||||
i = 0
|
i = 0
|
||||||
if not any(item["mix_tag"] == mix_tag for item in return_list):
|
if not any(item["mix_tag"] == mix_tag for item in resp):
|
||||||
return_list.append({'mix_tag': mix_tag,
|
resp.append({'mix_tag': mix_tag,
|
||||||
'profit': profit,
|
'profit_ratio': profit,
|
||||||
'profit_pct': round(profit * 100, 2),
|
'profit_pct': round(profit * 100, 2),
|
||||||
'profit_abs': profit_abs,
|
'profit_abs': profit_abs,
|
||||||
'count': count})
|
'count': count})
|
||||||
else:
|
else:
|
||||||
while i < len(return_list):
|
while i < len(resp):
|
||||||
if return_list[i]["mix_tag"] == mix_tag:
|
if resp[i]["mix_tag"] == mix_tag:
|
||||||
return_list[i] = {
|
resp[i] = {
|
||||||
'mix_tag': mix_tag,
|
'mix_tag': mix_tag,
|
||||||
'profit': profit + return_list[i]["profit"],
|
'profit_ratio': profit + resp[i]["profit_ratio"],
|
||||||
'profit_pct': round(profit + return_list[i]["profit"] * 100, 2),
|
'profit_pct': round(profit + resp[i]["profit_ratio"] * 100, 2),
|
||||||
'profit_abs': profit_abs + return_list[i]["profit_abs"],
|
'profit_abs': profit_abs + resp[i]["profit_abs"],
|
||||||
'count': 1 + return_list[i]["count"]}
|
'count': 1 + resp[i]["count"]
|
||||||
|
}
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
return return_list
|
return resp
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
|
def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from freqtrade.misc import pair_to_filename
|
|||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.strategy import IStrategy
|
from freqtrade.strategy import IStrategy
|
||||||
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -636,7 +637,7 @@ def load_and_plot_trades(config: Config):
|
|||||||
exchange = ExchangeResolver.load_exchange(config)
|
exchange = ExchangeResolver.load_exchange(config)
|
||||||
IStrategy.dp = DataProvider(config, exchange)
|
IStrategy.dp = DataProvider(config, exchange)
|
||||||
strategy.ft_bot_start()
|
strategy.ft_bot_start()
|
||||||
strategy.bot_loop_start(datetime.now(timezone.utc))
|
strategy_safe_wrapper(strategy.bot_loop_start)(current_time=datetime.now(timezone.utc))
|
||||||
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
|
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
|
||||||
timerange = plot_elements['timerange']
|
timerange = plot_elements['timerange']
|
||||||
trades = plot_elements['trades']
|
trades = plot_elements['trades']
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, RootModel, SerializeAsAny
|
from pydantic import BaseModel, RootModel, SerializeAsAny
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf
|
from freqtrade.constants import IntOrInf
|
||||||
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
|
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
|
||||||
from freqtrade.types import ValidExchangesType
|
from freqtrade.types import ValidExchangesType
|
||||||
|
|
||||||
@@ -95,15 +95,30 @@ class Count(BaseModel):
|
|||||||
total_stake: float
|
total_stake: float
|
||||||
|
|
||||||
|
|
||||||
class PerformanceEntry(BaseModel):
|
class __BaseStatsModel(BaseModel):
|
||||||
pair: str
|
|
||||||
profit: float
|
|
||||||
profit_ratio: float
|
profit_ratio: float
|
||||||
profit_pct: float
|
profit_pct: float
|
||||||
profit_abs: float
|
profit_abs: float
|
||||||
count: int
|
count: int
|
||||||
|
|
||||||
|
|
||||||
|
class Entry(__BaseStatsModel):
|
||||||
|
enter_tag: str
|
||||||
|
|
||||||
|
|
||||||
|
class Exit(__BaseStatsModel):
|
||||||
|
exit_reason: str
|
||||||
|
|
||||||
|
|
||||||
|
class MixTag(__BaseStatsModel):
|
||||||
|
mix_tag: str
|
||||||
|
|
||||||
|
|
||||||
|
class PerformanceEntry(__BaseStatsModel):
|
||||||
|
pair: str
|
||||||
|
profit: float
|
||||||
|
|
||||||
|
|
||||||
class Profit(BaseModel):
|
class Profit(BaseModel):
|
||||||
profit_closed_coin: float
|
profit_closed_coin: float
|
||||||
profit_closed_percent_mean: float
|
profit_closed_percent_mean: float
|
||||||
@@ -456,6 +471,7 @@ class FreqAIModelListResponse(BaseModel):
|
|||||||
class StrategyResponse(BaseModel):
|
class StrategyResponse(BaseModel):
|
||||||
strategy: str
|
strategy: str
|
||||||
code: str
|
code: str
|
||||||
|
timeframe: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class AvailablePairs(BaseModel):
|
class AvailablePairs(BaseModel):
|
||||||
@@ -484,11 +500,6 @@ class PairHistory(BaseModel):
|
|||||||
data_start: str
|
data_start: str
|
||||||
data_stop: str
|
data_stop: str
|
||||||
data_stop_ts: int
|
data_stop_ts: int
|
||||||
# TODO[pydantic]: The following keys were removed: `json_encoders`.
|
|
||||||
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information.
|
|
||||||
model_config = ConfigDict(json_encoders={
|
|
||||||
datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class BacktestFreqAIInputs(BaseModel):
|
class BacktestFreqAIInputs(BaseModel):
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ from freqtrade.exceptions import OperationalException
|
|||||||
from freqtrade.rpc import RPC
|
from freqtrade.rpc import RPC
|
||||||
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
|
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
|
||||||
BlacklistResponse, Count, DailyWeeklyMonthly,
|
BlacklistResponse, Count, DailyWeeklyMonthly,
|
||||||
DeleteLockRequest, DeleteTrade,
|
DeleteLockRequest, DeleteTrade, Entry,
|
||||||
ExchangeListResponse, ForceEnterPayload,
|
ExchangeListResponse, Exit, ForceEnterPayload,
|
||||||
ForceEnterResponse, ForceExitPayload,
|
ForceEnterResponse, ForceExitPayload,
|
||||||
FreqAIModelListResponse, Health, Locks, Logs,
|
FreqAIModelListResponse, Health, Locks, Logs,
|
||||||
OpenTradeSchema, PairHistory, PerformanceEntry,
|
MixTag, OpenTradeSchema, PairHistory,
|
||||||
Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
|
PerformanceEntry, Ping, PlotConfig, Profit,
|
||||||
Stats, StatusMsg, StrategyListResponse,
|
ResultMsg, ShowConfig, Stats, StatusMsg,
|
||||||
StrategyResponse, SysInfo, Version,
|
StrategyListResponse, StrategyResponse, SysInfo,
|
||||||
WhitelistResponse)
|
Version, WhitelistResponse)
|
||||||
from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional
|
from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional
|
||||||
from freqtrade.rpc.rpc import RPCException
|
from freqtrade.rpc.rpc import RPCException
|
||||||
|
|
||||||
@@ -52,7 +52,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# 2.31: new /backtest/history/ delete endpoint
|
# 2.31: new /backtest/history/ delete endpoint
|
||||||
# 2.32: new /backtest/history/ patch endpoint
|
# 2.32: new /backtest/history/ patch endpoint
|
||||||
# 2.33: Additional weekly/monthly metrics
|
# 2.33: Additional weekly/monthly metrics
|
||||||
API_VERSION = 2.33
|
# 2.34: new entries/exits/mix_tags endpoints
|
||||||
|
API_VERSION = 2.34
|
||||||
|
|
||||||
# Public API, requires no auth.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
@@ -83,6 +84,21 @@ def count(rpc: RPC = Depends(get_rpc)):
|
|||||||
return rpc._rpc_count()
|
return rpc._rpc_count()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/entries', response_model=List[Entry], tags=['info'])
|
||||||
|
def entries(pair: Optional[str] = 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: Optional[str] = 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: Optional[str] = None, rpc: RPC = Depends(get_rpc)):
|
||||||
|
return rpc._rpc_mix_tag_performance(pair)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/performance', response_model=List[PerformanceEntry], tags=['info'])
|
@router.get('/performance', response_model=List[PerformanceEntry], tags=['info'])
|
||||||
def performance(rpc: RPC = Depends(get_rpc)):
|
def performance(rpc: RPC = Depends(get_rpc)):
|
||||||
return rpc._rpc_performance()
|
return rpc._rpc_performance()
|
||||||
@@ -334,6 +350,7 @@ def get_strategy(strategy: str, config=Depends(get_config)):
|
|||||||
return {
|
return {
|
||||||
'strategy': strategy_obj.get_strategy_name(),
|
'strategy': strategy_obj.get_strategy_name(),
|
||||||
'code': strategy_obj.__source__,
|
'code': strategy_obj.__source__,
|
||||||
|
'timeframe': getattr(strategy_obj, 'timeframe', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ def get_exchange(config=Depends(get_config)):
|
|||||||
if not (exchange := ApiBG.exchanges.get(exchange_key)):
|
if not (exchange := ApiBG.exchanges.get(exchange_key)):
|
||||||
from freqtrade.resolvers import ExchangeResolver
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
exchange = ExchangeResolver.load_exchange(
|
exchange = ExchangeResolver.load_exchange(
|
||||||
config, load_leverage_tiers=False)
|
config, validate=False, load_leverage_tiers=False)
|
||||||
ApiBG.exchanges[exchange_key] = exchange
|
ApiBG.exchanges[exchange_key] = exchange
|
||||||
return exchange
|
return exchange
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class Discord(Webhook):
|
|||||||
self.rpc = rpc
|
self.rpc = rpc
|
||||||
self.strategy = config.get('strategy', '')
|
self.strategy = config.get('strategy', '')
|
||||||
self.timeframe = config.get('timeframe', '')
|
self.timeframe = config.get('timeframe', '')
|
||||||
|
self.bot_name = config.get('bot_name', '')
|
||||||
|
|
||||||
self._url = config['discord']['webhook_url']
|
self._url = config['discord']['webhook_url']
|
||||||
self._format = 'json'
|
self._format = 'json'
|
||||||
@@ -31,12 +32,12 @@ class Discord(Webhook):
|
|||||||
|
|
||||||
def send_msg(self, msg) -> None:
|
def send_msg(self, msg) -> None:
|
||||||
|
|
||||||
if msg['type'].value in self._config['discord']:
|
if (fields := self._config['discord'].get(msg['type'].value)):
|
||||||
logger.info(f"Sending discord message: {msg}")
|
logger.info(f"Sending discord message: {msg}")
|
||||||
|
|
||||||
msg['strategy'] = self.strategy
|
msg['strategy'] = self.strategy
|
||||||
msg['timeframe'] = self.timeframe
|
msg['timeframe'] = self.timeframe
|
||||||
fields = self._config['discord'].get(msg['type'].value)
|
msg['bot_name'] = self.bot_name
|
||||||
color = 0x0000FF
|
color = 0x0000FF
|
||||||
if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL):
|
if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL):
|
||||||
profit_ratio = msg.get('profit_ratio')
|
profit_ratio = msg.get('profit_ratio')
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ coingecko_mapping = {
|
|||||||
'busd': 'binance-usd',
|
'busd': 'binance-usd',
|
||||||
'tusd': 'true-usd',
|
'tusd': 'true-usd',
|
||||||
'usdc': 'usd-coin',
|
'usdc': 'usd-coin',
|
||||||
|
'btc': 'bitcoin'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ class RPC:
|
|||||||
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
||||||
'stake_amount': str(config['stake_amount']),
|
'stake_amount': str(config['stake_amount']),
|
||||||
'available_capital': config.get('available_capital'),
|
'available_capital': config.get('available_capital'),
|
||||||
'max_open_trades': (config['max_open_trades']
|
'max_open_trades': (config.get('max_open_trades', 0)
|
||||||
if config['max_open_trades'] != float('inf') else -1),
|
if config.get('max_open_trades', 0) != float('inf') else -1),
|
||||||
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
|
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
|
||||||
'stoploss': config.get('stoploss'),
|
'stoploss': config.get('stoploss'),
|
||||||
'stoploss_on_exchange': config.get('order_types',
|
'stoploss_on_exchange': config.get('order_types',
|
||||||
@@ -914,7 +914,8 @@ class RPC:
|
|||||||
|
|
||||||
if not stake_amount:
|
if not stake_amount:
|
||||||
# gen stake amount
|
# gen stake amount
|
||||||
stake_amount = self._freqtrade.wallets.get_trade_stake_amount(pair)
|
stake_amount = self._freqtrade.wallets.get_trade_stake_amount(
|
||||||
|
pair, self._config['max_open_trades'])
|
||||||
|
|
||||||
# execute buy
|
# execute buy
|
||||||
if not order_type:
|
if not order_type:
|
||||||
|
|||||||
+13
-10
@@ -5,6 +5,9 @@ from freqtrade.constants import PairWithTimeframe
|
|||||||
from freqtrade.enums import RPCMessageType
|
from freqtrade.enums import RPCMessageType
|
||||||
|
|
||||||
|
|
||||||
|
ProfitLossStr = Literal["profit", "loss"]
|
||||||
|
|
||||||
|
|
||||||
class RPCSendMsgBase(TypedDict):
|
class RPCSendMsgBase(TypedDict):
|
||||||
pass
|
pass
|
||||||
# ty1pe: Literal[RPCMessageType]
|
# ty1pe: Literal[RPCMessageType]
|
||||||
@@ -41,7 +44,7 @@ class RPCWhitelistMsg(RPCSendMsgBase):
|
|||||||
data: List[str]
|
data: List[str]
|
||||||
|
|
||||||
|
|
||||||
class __RPCBuyMsgBase(RPCSendMsgBase):
|
class __RPCEntryExitMsgBase(RPCSendMsgBase):
|
||||||
trade_id: int
|
trade_id: int
|
||||||
buy_tag: Optional[str]
|
buy_tag: Optional[str]
|
||||||
enter_tag: Optional[str]
|
enter_tag: Optional[str]
|
||||||
@@ -62,19 +65,19 @@ class __RPCBuyMsgBase(RPCSendMsgBase):
|
|||||||
sub_trade: bool
|
sub_trade: bool
|
||||||
|
|
||||||
|
|
||||||
class RPCBuyMsg(__RPCBuyMsgBase):
|
class RPCEntryMsg(__RPCEntryExitMsgBase):
|
||||||
type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]
|
type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]
|
||||||
|
|
||||||
|
|
||||||
class RPCCancelMsg(__RPCBuyMsgBase):
|
class RPCCancelMsg(__RPCEntryExitMsgBase):
|
||||||
type: Literal[RPCMessageType.ENTRY_CANCEL]
|
type: Literal[RPCMessageType.ENTRY_CANCEL]
|
||||||
reason: str
|
reason: str
|
||||||
|
|
||||||
|
|
||||||
class RPCSellMsg(__RPCBuyMsgBase):
|
class RPCExitMsg(__RPCEntryExitMsgBase):
|
||||||
type: Literal[RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]
|
type: Literal[RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]
|
||||||
cumulative_profit: float
|
cumulative_profit: float
|
||||||
gain: str # Literal["profit", "loss"]
|
gain: ProfitLossStr
|
||||||
close_rate: float
|
close_rate: float
|
||||||
profit_amount: float
|
profit_amount: float
|
||||||
profit_ratio: float
|
profit_ratio: float
|
||||||
@@ -85,10 +88,10 @@ class RPCSellMsg(__RPCBuyMsgBase):
|
|||||||
order_rate: Optional[float]
|
order_rate: Optional[float]
|
||||||
|
|
||||||
|
|
||||||
class RPCSellCancelMsg(__RPCBuyMsgBase):
|
class RPCExitCancelMsg(__RPCEntryExitMsgBase):
|
||||||
type: Literal[RPCMessageType.EXIT_CANCEL]
|
type: Literal[RPCMessageType.EXIT_CANCEL]
|
||||||
reason: str
|
reason: str
|
||||||
gain: str # Literal["profit", "loss"]
|
gain: ProfitLossStr
|
||||||
profit_amount: float
|
profit_amount: float
|
||||||
profit_ratio: float
|
profit_ratio: float
|
||||||
sell_reason: Optional[str]
|
sell_reason: Optional[str]
|
||||||
@@ -119,10 +122,10 @@ RPCSendMsg = Union[
|
|||||||
RPCStrategyMsg,
|
RPCStrategyMsg,
|
||||||
RPCProtectionMsg,
|
RPCProtectionMsg,
|
||||||
RPCWhitelistMsg,
|
RPCWhitelistMsg,
|
||||||
RPCBuyMsg,
|
RPCEntryMsg,
|
||||||
RPCCancelMsg,
|
RPCCancelMsg,
|
||||||
RPCSellMsg,
|
RPCExitMsg,
|
||||||
RPCSellCancelMsg,
|
RPCExitCancelMsg,
|
||||||
RPCAnalyzedDFMsg,
|
RPCAnalyzedDFMsg,
|
||||||
RPCNewCandleMsg
|
RPCNewCandleMsg
|
||||||
]
|
]
|
||||||
|
|||||||
+63
-15
@@ -223,7 +223,8 @@ class Telegram(RPCHandler):
|
|||||||
CommandHandler('health', self._health),
|
CommandHandler('health', self._health),
|
||||||
CommandHandler('help', self._help),
|
CommandHandler('help', self._help),
|
||||||
CommandHandler('version', self._version),
|
CommandHandler('version', self._version),
|
||||||
CommandHandler('marketdir', self._changemarketdir)
|
CommandHandler('marketdir', self._changemarketdir),
|
||||||
|
CommandHandler('order', self._order),
|
||||||
]
|
]
|
||||||
callbacks = [
|
callbacks = [
|
||||||
CallbackQueryHandler(self._status_table, pattern='update_status_table'),
|
CallbackQueryHandler(self._status_table, pattern='update_status_table'),
|
||||||
@@ -240,7 +241,7 @@ class Telegram(RPCHandler):
|
|||||||
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
||||||
CallbackQueryHandler(self._count, pattern='update_count'),
|
CallbackQueryHandler(self._count, pattern='update_count'),
|
||||||
CallbackQueryHandler(self._force_exit_inline, pattern=r"force_exit__\S+"),
|
CallbackQueryHandler(self._force_exit_inline, pattern=r"force_exit__\S+"),
|
||||||
CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"),
|
CallbackQueryHandler(self._force_enter_inline, pattern=r"force_enter__\S+"),
|
||||||
]
|
]
|
||||||
for handle in handles:
|
for handle in handles:
|
||||||
self._app.add_handler(handle)
|
self._app.add_handler(handle)
|
||||||
@@ -555,6 +556,47 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
return lines_detail
|
return lines_detail
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
async def _order(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /order.
|
||||||
|
Returns the orders of the trade
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
trade_ids = []
|
||||||
|
if context.args and len(context.args) > 0:
|
||||||
|
trade_ids = [int(i) for i in context.args if i.isnumeric()]
|
||||||
|
|
||||||
|
results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
|
||||||
|
for r in results:
|
||||||
|
lines = [
|
||||||
|
"*Order List for Trade #*`{trade_id}`"
|
||||||
|
]
|
||||||
|
|
||||||
|
lines_detail = self._prepare_order_details(
|
||||||
|
r['orders'], r['quote_currency'], r['is_open'])
|
||||||
|
lines.extend(lines_detail if lines_detail else "")
|
||||||
|
await self.__send_order_msg(lines, r)
|
||||||
|
|
||||||
|
async def __send_order_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Send status message.
|
||||||
|
"""
|
||||||
|
msg = ''
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if line:
|
||||||
|
if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
|
||||||
|
msg += line + '\n'
|
||||||
|
else:
|
||||||
|
await self._send_msg(msg.format(**r))
|
||||||
|
msg = "*Order List for Trade #*`{trade_id}` - continued\n" + line + '\n'
|
||||||
|
|
||||||
|
await self._send_msg(msg.format(**r))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
async def _status(self, update: Update, context: CallbackContext) -> None:
|
async def _status(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -652,9 +694,6 @@ class Telegram(RPCHandler):
|
|||||||
"*Open Order:* `{open_orders}`"
|
"*Open Order:* `{open_orders}`"
|
||||||
+ ("- `{exit_order_status}`" if r['exit_order_status'] else ""))
|
+ ("- `{exit_order_status}`" if r['exit_order_status'] else ""))
|
||||||
|
|
||||||
lines_detail = self._prepare_order_details(
|
|
||||||
r['orders'], r['quote_currency'], r['is_open'])
|
|
||||||
lines.extend(lines_detail if lines_detail else "")
|
|
||||||
await self.__send_status_msg(lines, r)
|
await self.__send_status_msg(lines, r)
|
||||||
|
|
||||||
async def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
|
async def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
|
||||||
@@ -1149,12 +1188,19 @@ class Telegram(RPCHandler):
|
|||||||
async def _force_enter_inline(self, update: Update, _: CallbackContext) -> None:
|
async def _force_enter_inline(self, update: Update, _: CallbackContext) -> None:
|
||||||
if update.callback_query:
|
if update.callback_query:
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
if query.data and '_||_' in query.data:
|
if query.data and '__' in query.data:
|
||||||
pair, side = query.data.split('_||_')
|
# Input data is "force_enter__<pair|cancel>_<side>"
|
||||||
order_side = SignalDirection(side)
|
payload = query.data.split("__")[1]
|
||||||
await query.answer()
|
if payload == 'cancel':
|
||||||
await query.edit_message_text(text=f"Manually entering {order_side} for {pair}")
|
await query.answer()
|
||||||
await self._force_enter_action(pair, None, order_side)
|
await query.edit_message_text(text="Force enter canceled.")
|
||||||
|
return
|
||||||
|
if payload and '_||_' in payload:
|
||||||
|
pair, side = payload.split('_||_')
|
||||||
|
order_side = SignalDirection(side)
|
||||||
|
await query.answer()
|
||||||
|
await query.edit_message_text(text=f"Manually entering {order_side} for {pair}")
|
||||||
|
await self._force_enter_action(pair, None, order_side)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _layout_inline_keyboard(
|
def _layout_inline_keyboard(
|
||||||
@@ -1183,12 +1229,14 @@ class Telegram(RPCHandler):
|
|||||||
else:
|
else:
|
||||||
whitelist = self._rpc._rpc_whitelist()['whitelist']
|
whitelist = self._rpc._rpc_whitelist()['whitelist']
|
||||||
pair_buttons = [
|
pair_buttons = [
|
||||||
InlineKeyboardButton(text=pair, callback_data=f"{pair}_||_{order_side}")
|
InlineKeyboardButton(
|
||||||
for pair in sorted(whitelist)
|
text=pair, callback_data=f"force_enter__{pair}_||_{order_side}"
|
||||||
|
) for pair in sorted(whitelist)
|
||||||
]
|
]
|
||||||
buttons_aligned = self._layout_inline_keyboard(pair_buttons)
|
buttons_aligned = self._layout_inline_keyboard(pair_buttons)
|
||||||
|
|
||||||
buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')])
|
buttons_aligned.append([InlineKeyboardButton(text='Cancel',
|
||||||
|
callback_data='force_enter__cancel')])
|
||||||
await self._send_msg(msg="Which pair?",
|
await self._send_msg(msg="Which pair?",
|
||||||
keyboard=buttons_aligned,
|
keyboard=buttons_aligned,
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
@@ -1369,7 +1417,7 @@ class Telegram(RPCHandler):
|
|||||||
stat_line = (
|
stat_line = (
|
||||||
f"{i+1}.\t <code>{trade['mix_tag']}\t"
|
f"{i+1}.\t <code>{trade['mix_tag']}\t"
|
||||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
f"({trade['profit']:.2%}) "
|
f"({trade['profit_ratio']:.2%}) "
|
||||||
f"({trade['count']})</code>\n")
|
f"({trade['count']})</code>\n")
|
||||||
|
|
||||||
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
|
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class Webhook(RPCHandler):
|
|||||||
valuedict = self._get_value_dict(msg)
|
valuedict = self._get_value_dict(msg)
|
||||||
|
|
||||||
if not valuedict:
|
if not valuedict:
|
||||||
logger.info("Message type '%s' not configured for webhooks", msg['type'])
|
logger.debug("Message type '%s' not configured for webhooks", msg['type'])
|
||||||
return
|
return
|
||||||
|
|
||||||
payload = {key: value.format(**msg) for (key, value) in valuedict.items()}
|
payload = {key: value.format(**msg) for (key, value) in valuedict.items()}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Callable, Optional, Union
|
from typing import Any, Callable, Dict, Optional, Union
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
@@ -38,17 +38,18 @@ def informative(timeframe: str, asset: str = '',
|
|||||||
|
|
||||||
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||||
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||||
current pair.
|
current pair. Also supports limited pair format strings (see below)
|
||||||
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||||
specified, defaults to:
|
specified, defaults to:
|
||||||
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||||
* {column}_{timeframe} if asset is not specified.
|
* {column}_{timeframe} if asset is not specified.
|
||||||
Format string supports these format variables:
|
Pair format supports these format variables:
|
||||||
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
|
||||||
* {base} - base currency in lower case, for example 'eth'.
|
* {base} - base currency in lower case, for example 'eth'.
|
||||||
* {BASE} - same as {base}, except in upper case.
|
* {BASE} - same as {base}, except in upper case.
|
||||||
* {quote} - quote currency in lower case, for example 'usdt'.
|
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||||
* {QUOTE} - same as {quote}, except in upper case.
|
* {QUOTE} - same as {quote}, except in upper case.
|
||||||
|
Format string additionally supports this variables.
|
||||||
|
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||||
* {column} - name of dataframe column.
|
* {column} - name of dataframe column.
|
||||||
* {timeframe} - timeframe of informative dataframe.
|
* {timeframe} - timeframe of informative dataframe.
|
||||||
:param ffill: ffill dataframe after merging informative pair.
|
:param ffill: ffill dataframe after merging informative pair.
|
||||||
@@ -68,9 +69,25 @@ def informative(timeframe: str, asset: str = '',
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def _format_pair_name(config, pair: str) -> str:
|
def __get_pair_formats(market: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||||
return pair.format(stake_currency=config['stake_currency'],
|
if not market:
|
||||||
stake=config['stake_currency']).upper()
|
return {}
|
||||||
|
base = market['base']
|
||||||
|
quote = market['quote']
|
||||||
|
return {
|
||||||
|
'base': base.lower(),
|
||||||
|
'BASE': base.upper(),
|
||||||
|
'quote': quote.lower(),
|
||||||
|
'QUOTE': quote.upper(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _format_pair_name(config, pair: str, market: Optional[Dict[str, Any]] = None) -> str:
|
||||||
|
return pair.format(
|
||||||
|
stake_currency=config['stake_currency'],
|
||||||
|
stake=config['stake_currency'],
|
||||||
|
**__get_pair_formats(market),
|
||||||
|
).upper()
|
||||||
|
|
||||||
|
|
||||||
def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict,
|
def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict,
|
||||||
@@ -85,7 +102,8 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata:
|
|||||||
|
|
||||||
if asset:
|
if asset:
|
||||||
# Insert stake currency if needed.
|
# Insert stake currency if needed.
|
||||||
asset = _format_pair_name(config, asset)
|
market1 = strategy.dp.market(metadata['pair'])
|
||||||
|
asset = _format_pair_name(config, asset, market1)
|
||||||
else:
|
else:
|
||||||
# Not specifying an asset will define informative dataframe for current pair.
|
# Not specifying an asset will define informative dataframe for current pair.
|
||||||
asset = metadata['pair']
|
asset = metadata['pair']
|
||||||
@@ -93,8 +111,6 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata:
|
|||||||
market = strategy.dp.market(asset)
|
market = strategy.dp.market(asset)
|
||||||
if market is None:
|
if market is None:
|
||||||
raise OperationalException(f'Market {asset} is not available.')
|
raise OperationalException(f'Market {asset} is not available.')
|
||||||
base = market['base']
|
|
||||||
quote = market['quote']
|
|
||||||
|
|
||||||
# Default format. This optimizes for the common case: informative pairs using same stake
|
# Default format. This optimizes for the common case: informative pairs using same stake
|
||||||
# currency. When quote currency matches stake currency, column name will omit base currency.
|
# currency. When quote currency matches stake currency, column name will omit base currency.
|
||||||
@@ -117,10 +133,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata:
|
|||||||
formatter = fmt.format # A default string formatter.
|
formatter = fmt.format # A default string formatter.
|
||||||
|
|
||||||
fmt_args = {
|
fmt_args = {
|
||||||
'BASE': base.upper(),
|
**__get_pair_formats(market),
|
||||||
'QUOTE': quote.upper(),
|
|
||||||
'base': base.lower(),
|
|
||||||
'quote': quote.lower(),
|
|
||||||
'asset': asset,
|
'asset': asset,
|
||||||
'timeframe': timeframe,
|
'timeframe': timeframe,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -756,12 +756,23 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
candle_type = (inf_data.candle_type if inf_data.candle_type
|
candle_type = (inf_data.candle_type if inf_data.candle_type
|
||||||
else self.config.get('candle_type_def', CandleType.SPOT))
|
else self.config.get('candle_type_def', CandleType.SPOT))
|
||||||
if inf_data.asset:
|
if inf_data.asset:
|
||||||
pair_tf = (
|
if any(s in inf_data.asset for s in ("{BASE}", "{base}")):
|
||||||
_format_pair_name(self.config, inf_data.asset),
|
for pair in self.dp.current_whitelist():
|
||||||
inf_data.timeframe,
|
|
||||||
candle_type,
|
pair_tf = (
|
||||||
)
|
_format_pair_name(self.config, inf_data.asset, self.dp.market(pair)),
|
||||||
informative_pairs.append(pair_tf)
|
inf_data.timeframe,
|
||||||
|
candle_type,
|
||||||
|
)
|
||||||
|
informative_pairs.append(pair_tf)
|
||||||
|
|
||||||
|
else:
|
||||||
|
pair_tf = (
|
||||||
|
_format_pair_name(self.config, inf_data.asset),
|
||||||
|
inf_data.timeframe,
|
||||||
|
candle_type,
|
||||||
|
)
|
||||||
|
informative_pairs.append(pair_tf)
|
||||||
else:
|
else:
|
||||||
for pair in self.dp.current_whitelist():
|
for pair in self.dp.current_whitelist():
|
||||||
informative_pairs.append((pair, inf_data.timeframe, candle_type))
|
informative_pairs.append((pair, inf_data.timeframe, candle_type))
|
||||||
@@ -1006,7 +1017,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
exit_ = latest.get(SignalType.EXIT_LONG.value, 0) == 1
|
exit_ = latest.get(SignalType.EXIT_LONG.value, 0) == 1
|
||||||
exit_tag = latest.get(SignalTagType.EXIT_TAG.value, None)
|
exit_tag = latest.get(SignalTagType.EXIT_TAG.value, None)
|
||||||
# Tags can be None, which does not resolve to False.
|
# Tags can be None, which does not resolve to False.
|
||||||
exit_tag = exit_tag if isinstance(exit_tag, str) else None
|
exit_tag = exit_tag if isinstance(exit_tag, str) and exit_tag != 'nan' else None
|
||||||
|
|
||||||
logger.debug(f"exit-trigger: {latest['date']} (pair={pair}) "
|
logger.debug(f"exit-trigger: {latest['date']} (pair={pair}) "
|
||||||
f"enter={enter} exit={exit_}")
|
f"enter={enter} exit={exit_}")
|
||||||
@@ -1038,17 +1049,17 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
exit_short = latest.get(SignalType.EXIT_SHORT.value, 0) == 1
|
exit_short = latest.get(SignalType.EXIT_SHORT.value, 0) == 1
|
||||||
|
|
||||||
enter_signal: Optional[SignalDirection] = None
|
enter_signal: Optional[SignalDirection] = None
|
||||||
enter_tag_value: Optional[str] = None
|
enter_tag: Optional[str] = None
|
||||||
if enter_long == 1 and not any([exit_long, enter_short]):
|
if enter_long == 1 and not any([exit_long, enter_short]):
|
||||||
enter_signal = SignalDirection.LONG
|
enter_signal = SignalDirection.LONG
|
||||||
enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None)
|
enter_tag = latest.get(SignalTagType.ENTER_TAG.value, None)
|
||||||
if (self.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT
|
if (self.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT
|
||||||
and self.can_short
|
and self.can_short
|
||||||
and enter_short == 1 and not any([exit_short, enter_long])):
|
and enter_short == 1 and not any([exit_short, enter_long])):
|
||||||
enter_signal = SignalDirection.SHORT
|
enter_signal = SignalDirection.SHORT
|
||||||
enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None)
|
enter_tag = latest.get(SignalTagType.ENTER_TAG.value, None)
|
||||||
|
|
||||||
enter_tag_value = enter_tag_value if isinstance(enter_tag_value, str) else None
|
enter_tag = enter_tag if isinstance(enter_tag, str) and enter_tag != 'nan' else None
|
||||||
|
|
||||||
timeframe_seconds = timeframe_to_seconds(timeframe)
|
timeframe_seconds = timeframe_to_seconds(timeframe)
|
||||||
|
|
||||||
@@ -1058,11 +1069,11 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
timeframe_seconds=timeframe_seconds,
|
timeframe_seconds=timeframe_seconds,
|
||||||
enter=bool(enter_signal)
|
enter=bool(enter_signal)
|
||||||
):
|
):
|
||||||
return None, enter_tag_value
|
return None, enter_tag
|
||||||
|
|
||||||
logger.debug(f"entry trigger: {latest['date']} (pair={pair}) "
|
logger.debug(f"entry trigger: {latest['date']} (pair={pair}) "
|
||||||
f"enter={enter_long} enter_tag_value={enter_tag_value}")
|
f"enter={enter_long} enter_tag_value={enter_tag}")
|
||||||
return enter_signal, enter_tag_value
|
return enter_signal, enter_tag
|
||||||
|
|
||||||
def ignore_expired_candle(
|
def ignore_expired_candle(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
|||||||
:return: Merged dataframe
|
:return: Merged dataframe
|
||||||
:raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe
|
:raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe
|
||||||
"""
|
"""
|
||||||
|
informative = informative.copy()
|
||||||
minutes_inf = timeframe_to_minutes(timeframe_inf)
|
minutes_inf = timeframe_to_minutes(timeframe_inf)
|
||||||
minutes = timeframe_to_minutes(timeframe)
|
minutes = timeframe_to_minutes(timeframe)
|
||||||
if minutes == minutes_inf:
|
if minutes == minutes_inf:
|
||||||
@@ -46,10 +46,16 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
|||||||
# Subtract "small" timeframe so merging is not delayed by 1 small candle
|
# Subtract "small" timeframe so merging is not delayed by 1 small candle
|
||||||
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
|
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
|
||||||
if not informative.empty:
|
if not informative.empty:
|
||||||
informative['date_merge'] = (
|
if timeframe_inf == '1M':
|
||||||
informative[date_column] + pd.to_timedelta(minutes_inf, 'm') -
|
informative['date_merge'] = (
|
||||||
pd.to_timedelta(minutes, 'm')
|
(informative[date_column] + pd.offsets.MonthBegin(1))
|
||||||
)
|
- pd.to_timedelta(minutes, 'm')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
informative['date_merge'] = (
|
||||||
|
informative[date_column] + pd.to_timedelta(minutes_inf, 'm') -
|
||||||
|
pd.to_timedelta(minutes, 'm')
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
informative['date_merge'] = informative[date_column]
|
informative['date_merge'] = informative[date_column]
|
||||||
else:
|
else:
|
||||||
@@ -80,9 +86,6 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
|||||||
right_on=date_merge, how='left')
|
right_on=date_merge, how='left')
|
||||||
dataframe = dataframe.drop(date_merge, axis=1)
|
dataframe = dataframe.drop(date_merge, axis=1)
|
||||||
|
|
||||||
# if ffill:
|
|
||||||
# dataframe = dataframe.ffill()
|
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import talib.abstract as ta
|
|||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from technical import qtpylib
|
from technical import qtpylib
|
||||||
|
|
||||||
from freqtrade.strategy import CategoricalParameter, IStrategy
|
from freqtrade.strategy import IStrategy
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -45,11 +45,6 @@ class FreqaiExampleStrategy(IStrategy):
|
|||||||
startup_candle_count: int = 40
|
startup_candle_count: int = 40
|
||||||
can_short = True
|
can_short = True
|
||||||
|
|
||||||
std_dev_multiplier_buy = CategoricalParameter(
|
|
||||||
[0.75, 1, 1.25, 1.5, 1.75], default=1.25, space="buy", optimize=True)
|
|
||||||
std_dev_multiplier_sell = CategoricalParameter(
|
|
||||||
[0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True)
|
|
||||||
|
|
||||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
|
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
|
||||||
metadata: Dict, **kwargs) -> DataFrame:
|
metadata: Dict, **kwargs) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@@ -239,21 +234,13 @@ class FreqaiExampleStrategy(IStrategy):
|
|||||||
|
|
||||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||||
|
|
||||||
for val in self.std_dev_multiplier_buy.range:
|
|
||||||
dataframe[f'target_roi_{val}'] = (
|
|
||||||
dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * val
|
|
||||||
)
|
|
||||||
for val in self.std_dev_multiplier_sell.range:
|
|
||||||
dataframe[f'sell_roi_{val}'] = (
|
|
||||||
dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * val
|
|
||||||
)
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
enter_long_conditions = [
|
enter_long_conditions = [
|
||||||
df["do_predict"] == 1,
|
df["do_predict"] == 1,
|
||||||
df["&-s_close"] > df[f"target_roi_{self.std_dev_multiplier_buy.value}"],
|
df["&-s_close"] > 0.01,
|
||||||
]
|
]
|
||||||
|
|
||||||
if enter_long_conditions:
|
if enter_long_conditions:
|
||||||
@@ -263,7 +250,7 @@ class FreqaiExampleStrategy(IStrategy):
|
|||||||
|
|
||||||
enter_short_conditions = [
|
enter_short_conditions = [
|
||||||
df["do_predict"] == 1,
|
df["do_predict"] == 1,
|
||||||
df["&-s_close"] < df[f"sell_roi_{self.std_dev_multiplier_sell.value}"],
|
df["&-s_close"] < -0.01,
|
||||||
]
|
]
|
||||||
|
|
||||||
if enter_short_conditions:
|
if enter_short_conditions:
|
||||||
@@ -276,23 +263,20 @@ class FreqaiExampleStrategy(IStrategy):
|
|||||||
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||||
exit_long_conditions = [
|
exit_long_conditions = [
|
||||||
df["do_predict"] == 1,
|
df["do_predict"] == 1,
|
||||||
df["&-s_close"] < df[f"sell_roi_{self.std_dev_multiplier_sell.value}"] * 0.25,
|
df["&-s_close"] < 0
|
||||||
]
|
]
|
||||||
if exit_long_conditions:
|
if exit_long_conditions:
|
||||||
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1
|
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1
|
||||||
|
|
||||||
exit_short_conditions = [
|
exit_short_conditions = [
|
||||||
df["do_predict"] == 1,
|
df["do_predict"] == 1,
|
||||||
df["&-s_close"] > df[f"target_roi_{self.std_dev_multiplier_buy.value}"] * 0.25,
|
df["&-s_close"] > 0
|
||||||
]
|
]
|
||||||
if exit_short_conditions:
|
if exit_short_conditions:
|
||||||
df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1
|
df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def get_ticker_indicator(self):
|
|
||||||
return int(self.config["timeframe"][:-1])
|
|
||||||
|
|
||||||
def confirm_trade_entry(
|
def confirm_trade_entry(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
},
|
},
|
||||||
{{ exchange | indent(4) }},
|
{{ exchange | indent(4) }},
|
||||||
"pairlists": [
|
"pairlists": [
|
||||||
{{ '{"method": "StaticPairList"}' if exchange_name == 'bittrex' else volume_pairlist }}
|
{{ volume_pairlist }}
|
||||||
],
|
],
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": {{ telegram | lower }},
|
"enabled": {{ telegram | lower }},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from copy import deepcopy
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Dict, NamedTuple, Optional
|
from typing import Dict, NamedTuple, Optional
|
||||||
|
|
||||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config
|
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config, IntOrInf
|
||||||
from freqtrade.enums import RunMode, TradingMode
|
from freqtrade.enums import RunMode, TradingMode
|
||||||
from freqtrade.exceptions import DependencyException
|
from freqtrade.exceptions import DependencyException
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
@@ -262,15 +262,15 @@ class Wallets:
|
|||||||
return min(self.get_total_stake_amount() - Trade.total_open_trades_stakes(), free)
|
return min(self.get_total_stake_amount() - Trade.total_open_trades_stakes(), free)
|
||||||
|
|
||||||
def _calculate_unlimited_stake_amount(self, available_amount: float,
|
def _calculate_unlimited_stake_amount(self, available_amount: float,
|
||||||
val_tied_up: float) -> float:
|
val_tied_up: float, max_open_trades: IntOrInf) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate stake amount for "unlimited" stake amount
|
Calculate stake amount for "unlimited" stake amount
|
||||||
:return: 0 if max number of trades reached, else stake_amount to use.
|
:return: 0 if max number of trades reached, else stake_amount to use.
|
||||||
"""
|
"""
|
||||||
if self._config['max_open_trades'] == 0:
|
if max_open_trades == 0:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades']
|
possible_stake = (available_amount + val_tied_up) / max_open_trades
|
||||||
# Theoretical amount can be above available amount - therefore limit to available amount!
|
# Theoretical amount can be above available amount - therefore limit to available amount!
|
||||||
return min(possible_stake, available_amount)
|
return min(possible_stake, available_amount)
|
||||||
|
|
||||||
@@ -298,7 +298,8 @@ class Wallets:
|
|||||||
|
|
||||||
return stake_amount
|
return stake_amount
|
||||||
|
|
||||||
def get_trade_stake_amount(self, pair: str, edge=None, update: bool = True) -> float:
|
def get_trade_stake_amount(
|
||||||
|
self, pair: str, max_open_trades: IntOrInf, edge=None, update: bool = True) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate stake amount for the trade
|
Calculate stake amount for the trade
|
||||||
:return: float: Stake amount
|
:return: float: Stake amount
|
||||||
@@ -322,7 +323,7 @@ class Wallets:
|
|||||||
stake_amount = self._config['stake_amount']
|
stake_amount = self._config['stake_amount']
|
||||||
if stake_amount == UNLIMITED_STAKE_AMOUNT:
|
if stake_amount == UNLIMITED_STAKE_AMOUNT:
|
||||||
stake_amount = self._calculate_unlimited_stake_amount(
|
stake_amount = self._calculate_unlimited_stake_amount(
|
||||||
available_amount, val_tied_up)
|
available_amount, val_tied_up, max_open_trades)
|
||||||
|
|
||||||
return self._check_available_stake_amount(stake_amount, available_amount)
|
return self._check_available_stake_amount(stake_amount, available_amount)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,55 @@
|
|||||||
requires = ["setuptools >= 64.0.0", "wheel"]
|
requires = ["setuptools >= 64.0.0", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "freqtrade"
|
||||||
|
dynamic = ["version", "dependencies", "optional-dependencies"]
|
||||||
|
|
||||||
|
authors = [
|
||||||
|
{name = "Freqtrade Team"},
|
||||||
|
{name = "Freqtrade Team", email = "freqtrade@protonmail.com"},
|
||||||
|
]
|
||||||
|
|
||||||
|
description = "Freqtrade - Crypto Trading Bot"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
license = {text = "GPLv3"}
|
||||||
|
# license = "GPLv3"
|
||||||
|
classifiers = [
|
||||||
|
"Environment :: Console",
|
||||||
|
"Intended Audience :: Science/Research",
|
||||||
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Operating System :: MacOS",
|
||||||
|
"Operating System :: Unix",
|
||||||
|
"Topic :: Office/Business :: Financial :: Investment",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/freqtrade/freqtrade"
|
||||||
|
Documentation = "https://freqtrade.io"
|
||||||
|
"Bug Tracker" = "https://github.com/freqtrade/freqtrade/issues"
|
||||||
|
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
freqtrade = "freqtrade.main:main"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
include-package-data = true
|
||||||
|
zip-safe = false
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["freqtrade*"]
|
||||||
|
exclude = ["tests", "tests.*"]
|
||||||
|
namespaces = true
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "freqtrade.__version__"}
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
exclude = '''
|
exclude = '''
|
||||||
@@ -31,6 +80,7 @@ skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*", "**/user_data/*"
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
|
addopts = "--dist loadscope"
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
@@ -93,3 +143,18 @@ max-complexity = 12
|
|||||||
|
|
||||||
[tool.ruff.per-file-ignores]
|
[tool.ruff.per-file-ignores]
|
||||||
"tests/*" = ["S"]
|
"tests/*" = ["S"]
|
||||||
|
|
||||||
|
[tool.flake8]
|
||||||
|
# Default from https://flake8.pycqa.org/en/latest/user/options.html#cmdoption-flake8-ignore
|
||||||
|
# minus E226
|
||||||
|
ignore = ["E121","E123","E126","E24","E704","W503","W504"]
|
||||||
|
max-line-length = 100
|
||||||
|
max-complexity = 12
|
||||||
|
exclude = [
|
||||||
|
".git",
|
||||||
|
"__pycache__",
|
||||||
|
".eggs",
|
||||||
|
"user_data",
|
||||||
|
".venv",
|
||||||
|
".env",
|
||||||
|
]
|
||||||
|
|||||||
@@ -7,23 +7,24 @@
|
|||||||
-r docs/requirements-docs.txt
|
-r docs/requirements-docs.txt
|
||||||
|
|
||||||
coveralls==3.3.1
|
coveralls==3.3.1
|
||||||
ruff==0.1.1
|
ruff==0.1.9
|
||||||
mypy==1.6.1
|
mypy==1.8.0
|
||||||
pre-commit==3.5.0
|
pre-commit==3.6.0
|
||||||
pytest==7.4.2
|
pytest==7.4.3
|
||||||
pytest-asyncio==0.21.1
|
pytest-asyncio==0.21.1
|
||||||
pytest-cov==4.1.0
|
pytest-cov==4.1.0
|
||||||
pytest-mock==3.12.0
|
pytest-mock==3.12.0
|
||||||
pytest-random-order==1.1.0
|
pytest-random-order==1.1.0
|
||||||
isort==5.12.0
|
pytest-xdist==3.5.0
|
||||||
|
isort==5.13.2
|
||||||
# For datetime mocking
|
# For datetime mocking
|
||||||
time-machine==2.13.0
|
time-machine==2.13.0
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==7.9.2
|
nbconvert==7.13.1
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.3.0.6
|
types-cachetools==5.3.0.7
|
||||||
types-filelock==3.2.7
|
types-filelock==3.2.7
|
||||||
types-requests==2.31.0.10
|
types-requests==2.31.0.10
|
||||||
types-tabulate==0.9.0.3
|
types-tabulate==0.9.0.3
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
-r requirements-freqai.txt
|
-r requirements-freqai.txt
|
||||||
|
|
||||||
# Required for freqai-rl
|
# Required for freqai-rl
|
||||||
torch==2.0.1
|
torch==2.1.2
|
||||||
#until these branches will be released we can use this
|
#until these branches will be released we can use this
|
||||||
gymnasium==0.29.1
|
gymnasium==0.29.1
|
||||||
stable_baselines3==2.1.0
|
stable_baselines3==2.2.1
|
||||||
sb3_contrib>=2.0.0a9
|
sb3_contrib>=2.0.0a9
|
||||||
# Progress bar for stable-baselines3 and sb3-contrib
|
# Progress bar for stable-baselines3 and sb3-contrib
|
||||||
tqdm==4.66.1
|
tqdm==4.66.1
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
-r requirements-plot.txt
|
-r requirements-plot.txt
|
||||||
|
|
||||||
# Required for freqai
|
# Required for freqai
|
||||||
scikit-learn==1.1.3
|
scikit-learn==1.3.2
|
||||||
joblib==1.3.2
|
joblib==1.3.2
|
||||||
catboost==1.2.2; 'arm' not in platform_machine
|
catboost==1.2.2; 'arm' not in platform_machine
|
||||||
lightgbm==4.1.0
|
lightgbm==4.2.0
|
||||||
xgboost==2.0.0
|
xgboost==2.0.3
|
||||||
tensorboard==2.15.0
|
tensorboard==2.15.1
|
||||||
datasieve==0.1.7
|
datasieve==0.1.7
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scipy==1.11.3
|
scipy==1.11.4
|
||||||
scikit-learn==1.1.3
|
scikit-learn==1.3.2
|
||||||
scikit-optimize==0.9.0
|
ft-scikit-optimize==0.9.2
|
||||||
filelock==3.12.4
|
filelock==3.13.1
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==5.17.0
|
plotly==5.18.0
|
||||||
|
|||||||
+22
-24
@@ -1,49 +1,47 @@
|
|||||||
numpy==1.25.2; platform_machine == 'armv7l'
|
numpy==1.26.2
|
||||||
numpy==1.26.1; platform_machine != 'armv7l'
|
pandas==2.1.4
|
||||||
pandas==2.0.3
|
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==4.1.22
|
ccxt==4.1.98
|
||||||
cryptography==41.0.4
|
cryptography==41.0.7
|
||||||
aiohttp==3.8.6
|
aiohttp==3.9.1
|
||||||
SQLAlchemy==2.0.22
|
SQLAlchemy==2.0.23
|
||||||
python-telegram-bot==20.6
|
python-telegram-bot==20.7
|
||||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||||
httpx>=0.24.1
|
httpx>=0.24.1
|
||||||
arrow==1.3.0
|
arrow==1.3.0
|
||||||
cachetools==5.3.1
|
cachetools==5.3.2
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
urllib3==2.0.7
|
urllib3==2.1.0
|
||||||
jsonschema==4.19.1
|
jsonschema==4.20.0
|
||||||
TA-Lib==0.4.28
|
TA-Lib==0.4.28
|
||||||
technical==1.4.0
|
technical==1.4.2
|
||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
pycoingecko==3.1.0
|
pycoingecko==3.1.0
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
tables==3.8.0
|
tables==3.9.1
|
||||||
blosc==1.11.1
|
|
||||||
joblib==1.3.2
|
joblib==1.3.2
|
||||||
rich==13.6.0
|
rich==13.7.0
|
||||||
pyarrow==13.0.0; platform_machine != 'armv7l'
|
pyarrow==14.0.2; platform_machine != 'armv7l'
|
||||||
|
|
||||||
# find first, C search in arrays
|
# find first, C search in arrays
|
||||||
py_find_1st==1.1.5
|
py_find_1st==1.1.6
|
||||||
|
|
||||||
# Load ticker files 30% faster
|
# Load ticker files 30% faster
|
||||||
python-rapidjson==1.12
|
python-rapidjson==1.14
|
||||||
# Properly format api responses
|
# Properly format api responses
|
||||||
orjson==3.9.9
|
orjson==3.9.10
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.104.0
|
fastapi==0.105.0
|
||||||
pydantic==2.4.2
|
pydantic==2.5.3
|
||||||
uvicorn==0.23.2
|
uvicorn==0.25.0
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
psutil==5.9.6
|
psutil==5.9.7
|
||||||
|
|
||||||
# Support for colorized terminal output
|
# Support for colorized terminal output
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
@@ -60,5 +58,5 @@ schedule==1.2.1
|
|||||||
websockets==12.0
|
websockets==12.0
|
||||||
janus==1.0.0
|
janus==1.0.0
|
||||||
|
|
||||||
ast-comments==1.1.2
|
ast-comments==1.2.0
|
||||||
packaging==23.2
|
packaging==23.2
|
||||||
|
|||||||
@@ -112,6 +112,30 @@ class FtRestClient:
|
|||||||
"""
|
"""
|
||||||
return self._get("count")
|
return self._get("count")
|
||||||
|
|
||||||
|
def entries(self, pair=None):
|
||||||
|
"""Returns List of dicts containing all Trades, based on buy tag performance
|
||||||
|
Can either be average for all pairs or a specific pair provided
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("entries", params={"pair": pair} if pair else None)
|
||||||
|
|
||||||
|
def exits(self, pair=None):
|
||||||
|
"""Returns List of dicts containing all Trades, based on exit reason performance
|
||||||
|
Can either be average for all pairs or a specific pair provided
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("exits", params={"pair": pair} if pair else None)
|
||||||
|
|
||||||
|
def mix_tags(self, pair=None):
|
||||||
|
"""Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance
|
||||||
|
Can either be average for all pairs or a specific pair provided
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("mix_tags", params={"pair": pair} if pair else None)
|
||||||
|
|
||||||
def locks(self):
|
def locks(self):
|
||||||
"""Return current locks
|
"""Return current locks
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
[metadata]
|
|
||||||
name = freqtrade
|
|
||||||
version = attr: freqtrade.__version__
|
|
||||||
author = Freqtrade Team
|
|
||||||
author_email = freqtrade@protonmail.com
|
|
||||||
description = Freqtrade - Crypto Trading Bot
|
|
||||||
long_description = file: README.md
|
|
||||||
long_description_content_type = text/markdown
|
|
||||||
url = https://github.com/freqtrade/freqtrade
|
|
||||||
project_urls =
|
|
||||||
Bug Tracker = https://github.com/freqtrade/freqtrade/issues
|
|
||||||
license = GPLv3
|
|
||||||
classifiers =
|
|
||||||
Environment :: Console
|
|
||||||
Intended Audience :: Science/Research
|
|
||||||
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
||||||
Programming Language :: Python :: 3.9
|
|
||||||
Programming Language :: Python :: 3.10
|
|
||||||
Programming Language :: Python :: 3.11
|
|
||||||
Operating System :: MacOS
|
|
||||||
Operating System :: Unix
|
|
||||||
Topic :: Office/Business :: Financial :: Investment
|
|
||||||
|
|
||||||
|
|
||||||
[options]
|
|
||||||
zip_safe = False
|
|
||||||
include_package_data = True
|
|
||||||
tests_require =
|
|
||||||
pytest
|
|
||||||
pytest-asyncio
|
|
||||||
pytest-cov
|
|
||||||
pytest-mock
|
|
||||||
|
|
||||||
packages = find:
|
|
||||||
python_requires = >=3.9
|
|
||||||
|
|
||||||
[options.entry_points]
|
|
||||||
console_scripts =
|
|
||||||
freqtrade = freqtrade.main:main
|
|
||||||
|
|
||||||
[flake8]
|
|
||||||
# Default from https://flake8.pycqa.org/en/latest/user/options.html#cmdoption-flake8-ignore
|
|
||||||
# minus E226
|
|
||||||
ignore = E121,E123,E126,E24,E704,W503,W504
|
|
||||||
max-line-length = 100
|
|
||||||
max-complexity = 12
|
|
||||||
exclude =
|
|
||||||
.git,
|
|
||||||
__pycache__,
|
|
||||||
.eggs,
|
|
||||||
user_data,
|
|
||||||
.venv
|
|
||||||
.env
|
|
||||||
@@ -5,8 +5,8 @@ from setuptools import setup
|
|||||||
plot = ['plotly>=4.0']
|
plot = ['plotly>=4.0']
|
||||||
hyperopt = [
|
hyperopt = [
|
||||||
'scipy',
|
'scipy',
|
||||||
'scikit-learn<=1.1.3',
|
'scikit-learn',
|
||||||
'scikit-optimize>=0.7.0',
|
'ft-scikit-optimize>=0.9.2',
|
||||||
'filelock',
|
'filelock',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -122,4 +122,5 @@ setup(
|
|||||||
'freqai_rl': freqai_rl,
|
'freqai_rl': freqai_rl,
|
||||||
'all': all_extra,
|
'all': all_extra,
|
||||||
},
|
},
|
||||||
|
url="https://github.com/freqtrade/freqtrade",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def test_validate_is_int():
|
|||||||
assert not validate_is_int('-ee')
|
assert not validate_is_int('-ee')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('exchange', ['bittrex', 'binance', 'kraken'])
|
@pytest.mark.parametrize('exchange', ['bybit', 'binance', 'kraken'])
|
||||||
def test_start_new_config(mocker, caplog, exchange):
|
def test_start_new_config(mocker, caplog, exchange):
|
||||||
wt_mock = mocker.patch.object(Path, "write_text", MagicMock())
|
wt_mock = mocker.patch.object(Path, "write_text", MagicMock())
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from tests.conftest_trades import MOCK_TRADE_COUNT
|
|||||||
|
|
||||||
def test_setup_utils_configuration():
|
def test_setup_utils_configuration():
|
||||||
args = [
|
args = [
|
||||||
'list-exchanges', '--config', 'config_examples/config_bittrex.example.json',
|
'list-exchanges', '--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
]
|
]
|
||||||
|
|
||||||
config = setup_utils_configuration(get_args(args), RunMode.OTHER)
|
config = setup_utils_configuration(get_args(args), RunMode.OTHER)
|
||||||
@@ -49,7 +49,7 @@ def test_start_trading_fail(mocker, caplog):
|
|||||||
exitmock = mocker.patch("freqtrade.worker.Worker.exit", MagicMock())
|
exitmock = mocker.patch("freqtrade.worker.Worker.exit", MagicMock())
|
||||||
args = [
|
args = [
|
||||||
'trade',
|
'trade',
|
||||||
'-c', 'config_examples/config_bittrex.example.json'
|
'-c', 'tests/testdata/testconfigs/main_test_config.json'
|
||||||
]
|
]
|
||||||
start_trading(get_args(args))
|
start_trading(get_args(args))
|
||||||
assert exitmock.call_count == 1
|
assert exitmock.call_count == 1
|
||||||
@@ -68,7 +68,7 @@ def test_start_webserver(mocker, caplog):
|
|||||||
|
|
||||||
args = [
|
args = [
|
||||||
'webserver',
|
'webserver',
|
||||||
'-c', 'config_examples/config_bittrex.example.json'
|
'-c', 'tests/testdata/testconfigs/main_test_config.json'
|
||||||
]
|
]
|
||||||
start_webserver(get_args(args))
|
start_webserver(get_args(args))
|
||||||
assert api_server_mock.call_count == 1
|
assert api_server_mock.call_count == 1
|
||||||
@@ -84,7 +84,7 @@ def test_list_exchanges(capsys):
|
|||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert re.match(r"Exchanges available for Freqtrade.*", captured.out)
|
assert re.match(r"Exchanges available for Freqtrade.*", captured.out)
|
||||||
assert re.search(r".*binance.*", captured.out)
|
assert re.search(r".*binance.*", captured.out)
|
||||||
assert re.search(r".*bittrex.*", captured.out)
|
assert re.search(r".*bybit.*", captured.out)
|
||||||
|
|
||||||
# Test with --one-column
|
# Test with --one-column
|
||||||
args = [
|
args = [
|
||||||
@@ -95,7 +95,7 @@ def test_list_exchanges(capsys):
|
|||||||
start_list_exchanges(get_args(args))
|
start_list_exchanges(get_args(args))
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert re.search(r"^binance$", captured.out, re.MULTILINE)
|
assert re.search(r"^binance$", captured.out, re.MULTILINE)
|
||||||
assert re.search(r"^bittrex$", captured.out, re.MULTILINE)
|
assert re.search(r"^bybit$", captured.out, re.MULTILINE)
|
||||||
|
|
||||||
# Test with --all
|
# Test with --all
|
||||||
args = [
|
args = [
|
||||||
@@ -107,7 +107,7 @@ def test_list_exchanges(capsys):
|
|||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert re.match(r"All exchanges supported by the ccxt library.*", captured.out)
|
assert re.match(r"All exchanges supported by the ccxt library.*", captured.out)
|
||||||
assert re.search(r".*binance.*", captured.out)
|
assert re.search(r".*binance.*", captured.out)
|
||||||
assert re.search(r".*bittrex.*", captured.out)
|
assert re.search(r".*bingx.*", captured.out)
|
||||||
assert re.search(r".*bitmex.*", captured.out)
|
assert re.search(r".*bitmex.*", captured.out)
|
||||||
|
|
||||||
# Test with --one-column --all
|
# Test with --one-column --all
|
||||||
@@ -120,7 +120,7 @@ def test_list_exchanges(capsys):
|
|||||||
start_list_exchanges(get_args(args))
|
start_list_exchanges(get_args(args))
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert re.search(r"^binance$", captured.out, re.MULTILINE)
|
assert re.search(r"^binance$", captured.out, re.MULTILINE)
|
||||||
assert re.search(r"^bittrex$", captured.out, re.MULTILINE)
|
assert re.search(r"^bingx$", captured.out, re.MULTILINE)
|
||||||
assert re.search(r"^bitmex$", captured.out, re.MULTILINE)
|
assert re.search(r"^bitmex$", captured.out, re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ def test_list_timeframes(mocker, capsys):
|
|||||||
'1h': 'hour',
|
'1h': 'hour',
|
||||||
'1d': 'day',
|
'1d': 'day',
|
||||||
}
|
}
|
||||||
patch_exchange(mocker, api_mock=api_mock, id='bittrex')
|
patch_exchange(mocker, api_mock=api_mock, id='bybit')
|
||||||
args = [
|
args = [
|
||||||
"list-timeframes",
|
"list-timeframes",
|
||||||
]
|
]
|
||||||
@@ -143,25 +143,25 @@ def test_list_timeframes(mocker, capsys):
|
|||||||
match=r"This command requires a configured exchange.*"):
|
match=r"This command requires a configured exchange.*"):
|
||||||
start_list_timeframes(pargs)
|
start_list_timeframes(pargs)
|
||||||
|
|
||||||
# Test with --config config_examples/config_bittrex.example.json
|
# Test with --config tests/testdata/testconfigs/main_test_config.json
|
||||||
args = [
|
args = [
|
||||||
"list-timeframes",
|
"list-timeframes",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
]
|
]
|
||||||
start_list_timeframes(get_args(args))
|
start_list_timeframes(get_args(args))
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert re.match("Timeframes available for the exchange `Bittrex`: "
|
assert re.match("Timeframes available for the exchange `Bybit`: "
|
||||||
"1m, 5m, 30m, 1h, 1d",
|
"1m, 5m, 30m, 1h, 1d",
|
||||||
captured.out)
|
captured.out)
|
||||||
|
|
||||||
# Test with --exchange bittrex
|
# Test with --exchange bybit
|
||||||
args = [
|
args = [
|
||||||
"list-timeframes",
|
"list-timeframes",
|
||||||
"--exchange", "bittrex",
|
"--exchange", "bybit",
|
||||||
]
|
]
|
||||||
start_list_timeframes(get_args(args))
|
start_list_timeframes(get_args(args))
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert re.match("Timeframes available for the exchange `Bittrex`: "
|
assert re.match("Timeframes available for the exchange `Bybit`: "
|
||||||
"1m, 5m, 30m, 1h, 1d",
|
"1m, 5m, 30m, 1h, 1d",
|
||||||
captured.out)
|
captured.out)
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ def test_list_timeframes(mocker, capsys):
|
|||||||
# Test with --one-column
|
# Test with --one-column
|
||||||
args = [
|
args = [
|
||||||
"list-timeframes",
|
"list-timeframes",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--one-column",
|
"--one-column",
|
||||||
]
|
]
|
||||||
start_list_timeframes(get_args(args))
|
start_list_timeframes(get_args(args))
|
||||||
@@ -217,7 +217,7 @@ def test_list_timeframes(mocker, capsys):
|
|||||||
def test_list_markets(mocker, markets_static, capsys):
|
def test_list_markets(mocker, markets_static, capsys):
|
||||||
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
patch_exchange(mocker, api_mock=api_mock, id='bittrex', mock_markets=markets_static)
|
patch_exchange(mocker, api_mock=api_mock, id='binance', mock_markets=markets_static)
|
||||||
|
|
||||||
# Test with no --config
|
# Test with no --config
|
||||||
args = [
|
args = [
|
||||||
@@ -229,15 +229,15 @@ def test_list_markets(mocker, markets_static, capsys):
|
|||||||
match=r"This command requires a configured exchange.*"):
|
match=r"This command requires a configured exchange.*"):
|
||||||
start_list_markets(pargs, False)
|
start_list_markets(pargs, False)
|
||||||
|
|
||||||
# Test with --config config_examples/config_bittrex.example.json
|
# Test with --config tests/testdata/testconfigs/main_test_config.json
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--print-list",
|
"--print-list",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 12 active markets: "
|
assert ("Exchange Binance has 12 active markets: "
|
||||||
"ADA/USDT:USDT, BLK/BTC, ETH/BTC, ETH/USDT, ETH/USDT:USDT, LTC/BTC, "
|
"ADA/USDT:USDT, BLK/BTC, ETH/BTC, ETH/USDT, ETH/USDT:USDT, LTC/BTC, "
|
||||||
"LTC/ETH, LTC/USD, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n"
|
"LTC/ETH, LTC/USD, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
@@ -255,16 +255,16 @@ def test_list_markets(mocker, markets_static, capsys):
|
|||||||
assert re.match("\nExchange Binance has 12 active markets:\n",
|
assert re.match("\nExchange Binance has 12 active markets:\n",
|
||||||
captured.out)
|
captured.out)
|
||||||
|
|
||||||
patch_exchange(mocker, api_mock=api_mock, id="bittrex", mock_markets=markets_static)
|
patch_exchange(mocker, api_mock=api_mock, id="binance", mock_markets=markets_static)
|
||||||
# Test with --all: all markets
|
# Test with --all: all markets
|
||||||
args = [
|
args = [
|
||||||
"list-markets", "--all",
|
"list-markets", "--all",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--print-list",
|
"--print-list",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 14 markets: "
|
assert ("Exchange Binance has 14 markets: "
|
||||||
"ADA/USDT:USDT, BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, ETH/USDT:USDT, "
|
"ADA/USDT:USDT, BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, ETH/USDT:USDT, "
|
||||||
"LTC/BTC, LTC/ETH, LTC/USD, LTC/USDT, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n"
|
"LTC/BTC, LTC/ETH, LTC/USD, LTC/USDT, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
@@ -272,24 +272,24 @@ def test_list_markets(mocker, markets_static, capsys):
|
|||||||
# Test list-pairs subcommand: active pairs
|
# Test list-pairs subcommand: active pairs
|
||||||
args = [
|
args = [
|
||||||
"list-pairs",
|
"list-pairs",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--print-list",
|
"--print-list",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), True)
|
start_list_markets(get_args(args), True)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 9 active pairs: "
|
assert ("Exchange Binance has 9 active pairs: "
|
||||||
"BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, NEO/BTC, TKN/BTC, XRP/BTC.\n"
|
"BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, NEO/BTC, TKN/BTC, XRP/BTC.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
|
|
||||||
# Test list-pairs subcommand with --all: all pairs
|
# Test list-pairs subcommand with --all: all pairs
|
||||||
args = [
|
args = [
|
||||||
"list-pairs", "--all",
|
"list-pairs", "--all",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--print-list",
|
"--print-list",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), True)
|
start_list_markets(get_args(args), True)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 11 pairs: "
|
assert ("Exchange Binance has 11 pairs: "
|
||||||
"BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, LTC/USDT, NEO/BTC, "
|
"BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, LTC/USDT, NEO/BTC, "
|
||||||
"TKN/BTC, XRP/BTC.\n"
|
"TKN/BTC, XRP/BTC.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
@@ -297,133 +297,133 @@ def test_list_markets(mocker, markets_static, capsys):
|
|||||||
# active markets, base=ETH, LTC
|
# active markets, base=ETH, LTC
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--base", "ETH", "LTC",
|
"--base", "ETH", "LTC",
|
||||||
"--print-list",
|
"--print-list",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 7 active markets with ETH, LTC as base currencies: "
|
assert ("Exchange Binance has 7 active markets with ETH, LTC as base currencies: "
|
||||||
"ETH/BTC, ETH/USDT, ETH/USDT:USDT, LTC/BTC, LTC/ETH, LTC/USD, XLTCUSDT.\n"
|
"ETH/BTC, ETH/USDT, ETH/USDT:USDT, LTC/BTC, LTC/ETH, LTC/USD, XLTCUSDT.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
|
|
||||||
# active markets, base=LTC
|
# active markets, base=LTC
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--base", "LTC",
|
"--base", "LTC",
|
||||||
"--print-list",
|
"--print-list",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 4 active markets with LTC as base currency: "
|
assert ("Exchange Binance has 4 active markets with LTC as base currency: "
|
||||||
"LTC/BTC, LTC/ETH, LTC/USD, XLTCUSDT.\n"
|
"LTC/BTC, LTC/ETH, LTC/USD, XLTCUSDT.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
|
|
||||||
# active markets, quote=USDT, USD
|
# active markets, quote=USDT, USD
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--quote", "USDT", "USD",
|
"--quote", "USDT", "USD",
|
||||||
"--print-list",
|
"--print-list",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 5 active markets with USDT, USD as quote currencies: "
|
assert ("Exchange Binance has 5 active markets with USDT, USD as quote currencies: "
|
||||||
"ADA/USDT:USDT, ETH/USDT, ETH/USDT:USDT, LTC/USD, XLTCUSDT.\n"
|
"ADA/USDT:USDT, ETH/USDT, ETH/USDT:USDT, LTC/USD, XLTCUSDT.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
|
|
||||||
# active markets, quote=USDT
|
# active markets, quote=USDT
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--quote", "USDT",
|
"--quote", "USDT",
|
||||||
"--print-list",
|
"--print-list",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 4 active markets with USDT as quote currency: "
|
assert ("Exchange Binance has 4 active markets with USDT as quote currency: "
|
||||||
"ADA/USDT:USDT, ETH/USDT, ETH/USDT:USDT, XLTCUSDT.\n"
|
"ADA/USDT:USDT, ETH/USDT, ETH/USDT:USDT, XLTCUSDT.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
|
|
||||||
# active markets, base=LTC, quote=USDT
|
# active markets, base=LTC, quote=USDT
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--base", "LTC", "--quote", "USDT",
|
"--base", "LTC", "--quote", "USDT",
|
||||||
"--print-list",
|
"--print-list",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 1 active market with LTC as base currency and "
|
assert ("Exchange Binance has 1 active market with LTC as base currency and "
|
||||||
"with USDT as quote currency: XLTCUSDT.\n"
|
"with USDT as quote currency: XLTCUSDT.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
|
|
||||||
# active pairs, base=LTC, quote=USDT
|
# active pairs, base=LTC, quote=USDT
|
||||||
args = [
|
args = [
|
||||||
"list-pairs",
|
"list-pairs",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--base", "LTC", "--quote", "USD",
|
"--base", "LTC", "--quote", "USD",
|
||||||
"--print-list",
|
"--print-list",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), True)
|
start_list_markets(get_args(args), True)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 1 active pair with LTC as base currency and "
|
assert ("Exchange Binance has 1 active pair with LTC as base currency and "
|
||||||
"with USD as quote currency: LTC/USD.\n"
|
"with USD as quote currency: LTC/USD.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
|
|
||||||
# active markets, base=LTC, quote=USDT, NONEXISTENT
|
# active markets, base=LTC, quote=USDT, NONEXISTENT
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--base", "LTC", "--quote", "USDT", "NONEXISTENT",
|
"--base", "LTC", "--quote", "USDT", "NONEXISTENT",
|
||||||
"--print-list",
|
"--print-list",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 1 active market with LTC as base currency and "
|
assert ("Exchange Binance has 1 active market with LTC as base currency and "
|
||||||
"with USDT, NONEXISTENT as quote currencies: XLTCUSDT.\n"
|
"with USDT, NONEXISTENT as quote currencies: XLTCUSDT.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
|
|
||||||
# active markets, base=LTC, quote=NONEXISTENT
|
# active markets, base=LTC, quote=NONEXISTENT
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--base", "LTC", "--quote", "NONEXISTENT",
|
"--base", "LTC", "--quote", "NONEXISTENT",
|
||||||
"--print-list",
|
"--print-list",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 0 active markets with LTC as base currency and "
|
assert ("Exchange Binance has 0 active markets with LTC as base currency and "
|
||||||
"with NONEXISTENT as quote currency.\n"
|
"with NONEXISTENT as quote currency.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
|
|
||||||
# Test tabular output
|
# Test tabular output
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 12 active markets:\n"
|
assert ("Exchange Binance has 12 active markets:\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
|
|
||||||
# Test tabular output, no markets found
|
# Test tabular output, no markets found
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--base", "LTC", "--quote", "NONEXISTENT",
|
"--base", "LTC", "--quote", "NONEXISTENT",
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert ("Exchange Bittrex has 0 active markets with LTC as base currency and "
|
assert ("Exchange Binance has 0 active markets with LTC as base currency and "
|
||||||
"with NONEXISTENT as quote currency.\n"
|
"with NONEXISTENT as quote currency.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
|
|
||||||
# Test --print-json
|
# Test --print-json
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--print-json"
|
"--print-json"
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
@@ -435,7 +435,7 @@ def test_list_markets(mocker, markets_static, capsys):
|
|||||||
# Test --print-csv
|
# Test --print-csv
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--print-csv"
|
"--print-csv"
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
@@ -447,7 +447,7 @@ def test_list_markets(mocker, markets_static, capsys):
|
|||||||
# Test --one-column
|
# Test --one-column
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--one-column"
|
"--one-column"
|
||||||
]
|
]
|
||||||
start_list_markets(get_args(args), False)
|
start_list_markets(get_args(args), False)
|
||||||
@@ -459,7 +459,7 @@ def test_list_markets(mocker, markets_static, capsys):
|
|||||||
# Test --one-column
|
# Test --one-column
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
'--config', 'config_examples/config_bittrex.example.json',
|
'--config', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
"--one-column"
|
"--one-column"
|
||||||
]
|
]
|
||||||
with pytest.raises(OperationalException, match=r"Cannot get markets.*"):
|
with pytest.raises(OperationalException, match=r"Cannot get markets.*"):
|
||||||
@@ -550,7 +550,7 @@ def test_start_install_ui(mocker):
|
|||||||
assert download_mock.call_count == 0
|
assert download_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_clean_ui_subdir(mocker, tmpdir, caplog):
|
def test_clean_ui_subdir(mocker, tmp_path, caplog):
|
||||||
mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir",
|
mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir",
|
||||||
side_effect=[True, True])
|
side_effect=[True, True])
|
||||||
mocker.patch("freqtrade.commands.deploy_commands.Path.is_file",
|
mocker.patch("freqtrade.commands.deploy_commands.Path.is_file",
|
||||||
@@ -560,14 +560,14 @@ def test_clean_ui_subdir(mocker, tmpdir, caplog):
|
|||||||
|
|
||||||
mocker.patch("freqtrade.commands.deploy_commands.Path.glob",
|
mocker.patch("freqtrade.commands.deploy_commands.Path.glob",
|
||||||
return_value=[Path('test1'), Path('test2'), Path('.gitkeep')])
|
return_value=[Path('test1'), Path('test2'), Path('.gitkeep')])
|
||||||
folder = Path(tmpdir) / "uitests"
|
folder = tmp_path / "uitests"
|
||||||
clean_ui_subdir(folder)
|
clean_ui_subdir(folder)
|
||||||
assert log_has("Removing UI directory content.", caplog)
|
assert log_has("Removing UI directory content.", caplog)
|
||||||
assert rd_mock.call_count == 1
|
assert rd_mock.call_count == 1
|
||||||
assert ul_mock.call_count == 1
|
assert ul_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_download_and_install_ui(mocker, tmpdir):
|
def test_download_and_install_ui(mocker, tmp_path):
|
||||||
# Create zipfile
|
# Create zipfile
|
||||||
requests_mock = MagicMock()
|
requests_mock = MagicMock()
|
||||||
file_like_object = BytesIO()
|
file_like_object = BytesIO()
|
||||||
@@ -583,7 +583,7 @@ def test_download_and_install_ui(mocker, tmpdir):
|
|||||||
side_effect=[True, False])
|
side_effect=[True, False])
|
||||||
wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes")
|
wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes")
|
||||||
|
|
||||||
folder = Path(tmpdir) / "uitests_dl"
|
folder = tmp_path / "uitests_dl"
|
||||||
folder.mkdir(exist_ok=True)
|
folder.mkdir(exist_ok=True)
|
||||||
|
|
||||||
assert read_ui_version(folder) is None
|
assert read_ui_version(folder) is None
|
||||||
@@ -971,7 +971,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
|||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
args = [
|
args = [
|
||||||
'test-pairlist',
|
'test-pairlist',
|
||||||
'-c', 'config_examples/config_bittrex.example.json'
|
'-c', 'tests/testdata/testconfigs/main_test_config.json'
|
||||||
]
|
]
|
||||||
|
|
||||||
start_test_pairlist(get_args(args))
|
start_test_pairlist(get_args(args))
|
||||||
@@ -985,7 +985,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
|||||||
|
|
||||||
args = [
|
args = [
|
||||||
'test-pairlist',
|
'test-pairlist',
|
||||||
'-c', 'config_examples/config_bittrex.example.json',
|
'-c', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
'--one-column',
|
'--one-column',
|
||||||
]
|
]
|
||||||
start_test_pairlist(get_args(args))
|
start_test_pairlist(get_args(args))
|
||||||
@@ -994,7 +994,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
|||||||
|
|
||||||
args = [
|
args = [
|
||||||
'test-pairlist',
|
'test-pairlist',
|
||||||
'-c', 'config_examples/config_bittrex.example.json',
|
'-c', 'tests/testdata/testconfigs/main_test_config.json',
|
||||||
'--print-json',
|
'--print-json',
|
||||||
]
|
]
|
||||||
start_test_pairlist(get_args(args))
|
start_test_pairlist(get_args(args))
|
||||||
@@ -1010,8 +1010,8 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
|||||||
pytest.fail(f'Expected well formed JSON, but failed to parse: {captured.out}')
|
pytest.fail(f'Expected well formed JSON, but failed to parse: {captured.out}')
|
||||||
|
|
||||||
|
|
||||||
def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir):
|
def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmp_path):
|
||||||
csv_file = Path(tmpdir) / "test.csv"
|
csv_file = tmp_path / "test.csv"
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist',
|
'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist',
|
||||||
return_value=True
|
return_value=True
|
||||||
@@ -1512,10 +1512,10 @@ def test_backtesting_show(mocker, testdatadir, capsys):
|
|||||||
assert "Pairs for Strategy" in out
|
assert "Pairs for Strategy" in out
|
||||||
|
|
||||||
|
|
||||||
def test_start_convert_db(mocker, fee, tmpdir, caplog):
|
def test_start_convert_db(fee, tmp_path):
|
||||||
db_src_file = Path(f"{tmpdir}/db.sqlite")
|
db_src_file = tmp_path / "db.sqlite"
|
||||||
db_from = f"sqlite:///{db_src_file}"
|
db_from = f"sqlite:///{db_src_file}"
|
||||||
db_target_file = Path(f"{tmpdir}/db_target.sqlite")
|
db_target_file = tmp_path / "db_target.sqlite"
|
||||||
db_to = f"sqlite:///{db_target_file}"
|
db_to = f"sqlite:///{db_target_file}"
|
||||||
args = [
|
args = [
|
||||||
"convert-db",
|
"convert-db",
|
||||||
@@ -1542,13 +1542,13 @@ def test_start_convert_db(mocker, fee, tmpdir, caplog):
|
|||||||
assert db_target_file.is_file()
|
assert db_target_file.is_file()
|
||||||
|
|
||||||
|
|
||||||
def test_start_strategy_updater(mocker, tmpdir):
|
def test_start_strategy_updater(mocker, tmp_path):
|
||||||
sc_mock = mocker.patch('freqtrade.commands.strategy_utils_commands.start_conversion')
|
sc_mock = mocker.patch('freqtrade.commands.strategy_utils_commands.start_conversion')
|
||||||
teststrats = Path(__file__).parent.parent / 'strategy/strats'
|
teststrats = Path(__file__).parent.parent / 'strategy/strats'
|
||||||
args = [
|
args = [
|
||||||
"strategy-updater",
|
"strategy-updater",
|
||||||
"--userdir",
|
"--userdir",
|
||||||
str(tmpdir),
|
str(tmp_path),
|
||||||
"--strategy-path",
|
"--strategy-path",
|
||||||
str(teststrats),
|
str(teststrats),
|
||||||
]
|
]
|
||||||
@@ -1562,7 +1562,7 @@ def test_start_strategy_updater(mocker, tmpdir):
|
|||||||
args = [
|
args = [
|
||||||
"strategy-updater",
|
"strategy-updater",
|
||||||
"--userdir",
|
"--userdir",
|
||||||
str(tmpdir),
|
str(tmp_path),
|
||||||
"--strategy-path",
|
"--strategy-path",
|
||||||
str(teststrats),
|
str(teststrats),
|
||||||
"--strategy-list",
|
"--strategy-list",
|
||||||
|
|||||||
+31
-5
@@ -11,6 +11,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
from xdist.scheduler.loadscope import LoadScopeScheduling
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.commands import Arguments
|
from freqtrade.commands import Arguments
|
||||||
@@ -56,6 +57,27 @@ def pytest_configure(config):
|
|||||||
setattr(config.option, 'markexpr', 'not longrun')
|
setattr(config.option, 'markexpr', 'not longrun')
|
||||||
|
|
||||||
|
|
||||||
|
class FixtureScheduler(LoadScopeScheduling):
|
||||||
|
# Based on the suggestion in
|
||||||
|
# https://github.com/pytest-dev/pytest-xdist/issues/18
|
||||||
|
|
||||||
|
def _split_scope(self, nodeid):
|
||||||
|
if 'exchange_online' in nodeid:
|
||||||
|
try:
|
||||||
|
# Extract exchange ID from nodeid
|
||||||
|
exchange_id = nodeid.split('[')[1].split('-')[0].rstrip(']')
|
||||||
|
return exchange_id
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
pass
|
||||||
|
|
||||||
|
return nodeid
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_xdist_make_scheduler(config, log):
|
||||||
|
return FixtureScheduler(config, log)
|
||||||
|
|
||||||
|
|
||||||
def log_has(line, logs):
|
def log_has(line, logs):
|
||||||
"""Check if line is found on some caplog's message."""
|
"""Check if line is found on some caplog's message."""
|
||||||
return any(line == message for message in logs.messages)
|
return any(line == message for message in logs.messages)
|
||||||
@@ -87,11 +109,15 @@ def get_args(args):
|
|||||||
|
|
||||||
def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'):
|
def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'):
|
||||||
np.random.seed(42)
|
np.random.seed(42)
|
||||||
tf_mins = timeframe_to_minutes(timeframe)
|
|
||||||
|
|
||||||
base = np.random.normal(20, 2, size=size)
|
base = np.random.normal(20, 2, size=size)
|
||||||
|
if timeframe == '1M':
|
||||||
date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC')
|
date = pd.date_range(start, periods=size, freq='1MS', tz='UTC')
|
||||||
|
elif timeframe == '1w':
|
||||||
|
date = pd.date_range(start, periods=size, freq='1W-MON', tz='UTC')
|
||||||
|
else:
|
||||||
|
tf_mins = timeframe_to_minutes(timeframe)
|
||||||
|
date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC')
|
||||||
df = pd.DataFrame({
|
df = pd.DataFrame({
|
||||||
'date': date,
|
'date': date,
|
||||||
'open': base,
|
'open': base,
|
||||||
@@ -413,8 +439,8 @@ def patch_gc(mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def user_dir(mocker, tmpdir) -> Path:
|
def user_dir(mocker, tmp_path) -> Path:
|
||||||
user_dir = Path(tmpdir) / "user_data"
|
user_dir = tmp_path / "user_data"
|
||||||
mocker.patch('freqtrade.configuration.configuration.create_userdata_dir',
|
mocker.patch('freqtrade.configuration.configuration.create_userdata_dir',
|
||||||
return_value=user_dir)
|
return_value=user_dir)
|
||||||
return user_dir
|
return user_dir
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -50,8 +49,8 @@ def test_trades_to_ohlcv(trades_history_df, caplog):
|
|||||||
assert 'high' in df.columns
|
assert 'high' in df.columns
|
||||||
assert 'low' in df.columns
|
assert 'low' in df.columns
|
||||||
assert 'close' in df.columns
|
assert 'close' in df.columns
|
||||||
assert df.loc[:, 'high'][0] == 0.019627
|
assert df.iloc[0, :]['high'] == 0.019627
|
||||||
assert df.loc[:, 'low'][0] == 0.019626
|
assert df.iloc[0, :]['low'] == 0.019626
|
||||||
|
|
||||||
|
|
||||||
def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
|
def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
|
||||||
@@ -65,7 +64,7 @@ def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
|
|||||||
# Column names should not change
|
# Column names should not change
|
||||||
assert (data.columns == data2.columns).all()
|
assert (data.columns == data2.columns).all()
|
||||||
|
|
||||||
assert log_has_re(f"Missing data fillup for UNITTEST/BTC: before: "
|
assert log_has_re(f"Missing data fillup for UNITTEST/BTC, 1m: before: "
|
||||||
f"{len(data)} - after: {len(data2)}.*", caplog)
|
f"{len(data)} - after: {len(data2)}.*", caplog)
|
||||||
|
|
||||||
# Test fillup actually fixes invalid backtest data
|
# Test fillup actually fixes invalid backtest data
|
||||||
@@ -129,7 +128,7 @@ def test_ohlcv_fill_up_missing_data2(caplog):
|
|||||||
# Column names should not change
|
# Column names should not change
|
||||||
assert (data.columns == data2.columns).all()
|
assert (data.columns == data2.columns).all()
|
||||||
|
|
||||||
assert log_has_re(f"Missing data fillup for UNITTEST/BTC: before: "
|
assert log_has_re(f"Missing data fillup for UNITTEST/BTC, {timeframe}: before: "
|
||||||
f"{len(data)} - after: {len(data2)}.*", caplog)
|
f"{len(data)} - after: {len(data2)}.*", caplog)
|
||||||
|
|
||||||
|
|
||||||
@@ -323,18 +322,17 @@ def test_trades_dict_to_list(fetch_trades_result):
|
|||||||
assert t[6] == fetch_trades_result[i]['cost']
|
assert t[6] == fetch_trades_result[i]['cost']
|
||||||
|
|
||||||
|
|
||||||
def test_convert_trades_format(default_conf, testdatadir, tmpdir):
|
def test_convert_trades_format(default_conf, testdatadir, tmp_path):
|
||||||
tmpdir1 = Path(tmpdir)
|
files = [{'old': tmp_path / "XRP_ETH-trades.json.gz",
|
||||||
files = [{'old': tmpdir1 / "XRP_ETH-trades.json.gz",
|
'new': tmp_path / "XRP_ETH-trades.json"},
|
||||||
'new': tmpdir1 / "XRP_ETH-trades.json"},
|
{'old': tmp_path / "XRP_OLD-trades.json.gz",
|
||||||
{'old': tmpdir1 / "XRP_OLD-trades.json.gz",
|
'new': tmp_path / "XRP_OLD-trades.json"},
|
||||||
'new': tmpdir1 / "XRP_OLD-trades.json"},
|
|
||||||
]
|
]
|
||||||
for file in files:
|
for file in files:
|
||||||
copyfile(testdatadir / file['old'].name, file['old'])
|
copyfile(testdatadir / file['old'].name, file['old'])
|
||||||
assert not file['new'].exists()
|
assert not file['new'].exists()
|
||||||
|
|
||||||
default_conf['datadir'] = tmpdir1
|
default_conf['datadir'] = tmp_path
|
||||||
|
|
||||||
convert_trades_format(default_conf, convert_from='jsongz',
|
convert_trades_format(default_conf, convert_from='jsongz',
|
||||||
convert_to='json', erase=False)
|
convert_to='json', erase=False)
|
||||||
@@ -362,16 +360,15 @@ def test_convert_trades_format(default_conf, testdatadir, tmpdir):
|
|||||||
(['UNITTEST_USDT_USDT-1h-mark', 'XRP_USDT_USDT-1h-mark'], CandleType.MARK),
|
(['UNITTEST_USDT_USDT-1h-mark', 'XRP_USDT_USDT-1h-mark'], CandleType.MARK),
|
||||||
(['XRP_USDT_USDT-1h-futures'], CandleType.FUTURES),
|
(['XRP_USDT_USDT-1h-futures'], CandleType.FUTURES),
|
||||||
])
|
])
|
||||||
def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, candletype):
|
def test_convert_ohlcv_format(default_conf, testdatadir, tmp_path, file_base, candletype):
|
||||||
tmpdir1 = Path(tmpdir)
|
|
||||||
prependix = '' if candletype == CandleType.SPOT else 'futures/'
|
prependix = '' if candletype == CandleType.SPOT else 'futures/'
|
||||||
files_orig = []
|
files_orig = []
|
||||||
files_temp = []
|
files_temp = []
|
||||||
files_new = []
|
files_new = []
|
||||||
for file in file_base:
|
for file in file_base:
|
||||||
file_orig = testdatadir / f"{prependix}{file}.feather"
|
file_orig = testdatadir / f"{prependix}{file}.feather"
|
||||||
file_temp = tmpdir1 / f"{prependix}{file}.feather"
|
file_temp = tmp_path / f"{prependix}{file}.feather"
|
||||||
file_new = tmpdir1 / f"{prependix}{file}.json.gz"
|
file_new = tmp_path / f"{prependix}{file}.json.gz"
|
||||||
IDataHandler.create_dir_if_needed(file_temp)
|
IDataHandler.create_dir_if_needed(file_temp)
|
||||||
copyfile(file_orig, file_temp)
|
copyfile(file_orig, file_temp)
|
||||||
|
|
||||||
@@ -379,7 +376,7 @@ def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, cand
|
|||||||
files_temp.append(file_temp)
|
files_temp.append(file_temp)
|
||||||
files_new.append(file_new)
|
files_new.append(file_new)
|
||||||
|
|
||||||
default_conf['datadir'] = tmpdir1
|
default_conf['datadir'] = tmp_path
|
||||||
default_conf['candle_types'] = [candletype]
|
default_conf['candle_types'] = [candletype]
|
||||||
|
|
||||||
if candletype == CandleType.SPOT:
|
if candletype == CandleType.SPOT:
|
||||||
@@ -445,30 +442,29 @@ def test_reduce_dataframe_footprint():
|
|||||||
assert df2['close_copy'].dtype == np.float32
|
assert df2['close_copy'].dtype == np.float32
|
||||||
|
|
||||||
|
|
||||||
def test_convert_trades_to_ohlcv(testdatadir, tmpdir, caplog):
|
def test_convert_trades_to_ohlcv(testdatadir, tmp_path, caplog):
|
||||||
tmpdir1 = Path(tmpdir)
|
|
||||||
pair = 'XRP/ETH'
|
pair = 'XRP/ETH'
|
||||||
file1 = tmpdir1 / 'XRP_ETH-1m.feather'
|
file1 = tmp_path / 'XRP_ETH-1m.feather'
|
||||||
file5 = tmpdir1 / 'XRP_ETH-5m.feather'
|
file5 = tmp_path / 'XRP_ETH-5m.feather'
|
||||||
filetrades = tmpdir1 / 'XRP_ETH-trades.json.gz'
|
filetrades = tmp_path / 'XRP_ETH-trades.json.gz'
|
||||||
copyfile(testdatadir / file1.name, file1)
|
copyfile(testdatadir / file1.name, file1)
|
||||||
copyfile(testdatadir / file5.name, file5)
|
copyfile(testdatadir / file5.name, file5)
|
||||||
copyfile(testdatadir / filetrades.name, filetrades)
|
copyfile(testdatadir / filetrades.name, filetrades)
|
||||||
|
|
||||||
# Compare downloaded dataset with converted dataset
|
# Compare downloaded dataset with converted dataset
|
||||||
dfbak_1m = load_pair_history(datadir=tmpdir1, timeframe="1m", pair=pair)
|
dfbak_1m = load_pair_history(datadir=tmp_path, timeframe="1m", pair=pair)
|
||||||
dfbak_5m = load_pair_history(datadir=tmpdir1, timeframe="5m", pair=pair)
|
dfbak_5m = load_pair_history(datadir=tmp_path, timeframe="5m", pair=pair)
|
||||||
|
|
||||||
tr = TimeRange.parse_timerange('20191011-20191012')
|
tr = TimeRange.parse_timerange('20191011-20191012')
|
||||||
|
|
||||||
convert_trades_to_ohlcv([pair], timeframes=['1m', '5m'],
|
convert_trades_to_ohlcv([pair], timeframes=['1m', '5m'],
|
||||||
data_format_trades='jsongz',
|
data_format_trades='jsongz',
|
||||||
datadir=tmpdir1, timerange=tr, erase=True)
|
datadir=tmp_path, timerange=tr, erase=True)
|
||||||
|
|
||||||
assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog)
|
assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog)
|
||||||
# Load new data
|
# Load new data
|
||||||
df_1m = load_pair_history(datadir=tmpdir1, timeframe="1m", pair=pair)
|
df_1m = load_pair_history(datadir=tmp_path, timeframe="1m", pair=pair)
|
||||||
df_5m = load_pair_history(datadir=tmpdir1, timeframe="5m", pair=pair)
|
df_5m = load_pair_history(datadir=tmp_path, timeframe="5m", pair=pair)
|
||||||
|
|
||||||
assert_frame_equal(dfbak_1m, df_1m, check_exact=True)
|
assert_frame_equal(dfbak_1m, df_1m, check_exact=True)
|
||||||
assert_frame_equal(dfbak_5m, df_5m, check_exact=True)
|
assert_frame_equal(dfbak_5m, df_5m, check_exact=True)
|
||||||
@@ -477,5 +473,5 @@ def test_convert_trades_to_ohlcv(testdatadir, tmpdir, caplog):
|
|||||||
|
|
||||||
convert_trades_to_ohlcv(['NoDatapair'], timeframes=['1m', '5m'],
|
convert_trades_to_ohlcv(['NoDatapair'], timeframes=['1m', '5m'],
|
||||||
data_format_trades='jsongz',
|
data_format_trades='jsongz',
|
||||||
datadir=tmpdir1, timerange=tr, erase=True)
|
datadir=tmp_path, timerange=tr, erase=True)
|
||||||
assert log_has(msg, caplog)
|
assert log_has(msg, caplog)
|
||||||
|
|||||||
@@ -328,17 +328,16 @@ def test_hdf5datahandler_trades_load(testdatadir):
|
|||||||
])
|
])
|
||||||
def test_hdf5datahandler_ohlcv_load_and_resave(
|
def test_hdf5datahandler_ohlcv_load_and_resave(
|
||||||
testdatadir,
|
testdatadir,
|
||||||
tmpdir,
|
tmp_path,
|
||||||
pair,
|
pair,
|
||||||
timeframe,
|
timeframe,
|
||||||
candle_type,
|
candle_type,
|
||||||
candle_append,
|
candle_append,
|
||||||
startdt, enddt
|
startdt, enddt
|
||||||
):
|
):
|
||||||
tmpdir1 = Path(tmpdir)
|
tmpdir2 = tmp_path
|
||||||
tmpdir2 = tmpdir1
|
|
||||||
if candle_type not in ('', 'spot'):
|
if candle_type not in ('', 'spot'):
|
||||||
tmpdir2 = tmpdir1 / 'futures'
|
tmpdir2 = tmp_path / 'futures'
|
||||||
tmpdir2.mkdir()
|
tmpdir2.mkdir()
|
||||||
dh = get_datahandler(testdatadir, 'hdf5')
|
dh = get_datahandler(testdatadir, 'hdf5')
|
||||||
ohlcv = dh._ohlcv_load(pair, timeframe, None, candle_type=candle_type)
|
ohlcv = dh._ohlcv_load(pair, timeframe, None, candle_type=candle_type)
|
||||||
@@ -348,7 +347,7 @@ def test_hdf5datahandler_ohlcv_load_and_resave(
|
|||||||
file = tmpdir2 / f"UNITTEST_NEW-{timeframe}{candle_append}.h5"
|
file = tmpdir2 / f"UNITTEST_NEW-{timeframe}{candle_append}.h5"
|
||||||
assert not file.is_file()
|
assert not file.is_file()
|
||||||
|
|
||||||
dh1 = get_datahandler(tmpdir1, 'hdf5')
|
dh1 = get_datahandler(tmp_path, 'hdf5')
|
||||||
dh1.ohlcv_store('UNITTEST/NEW', timeframe, ohlcv, candle_type=candle_type)
|
dh1.ohlcv_store('UNITTEST/NEW', timeframe, ohlcv, candle_type=candle_type)
|
||||||
assert file.is_file()
|
assert file.is_file()
|
||||||
|
|
||||||
@@ -379,17 +378,16 @@ def test_hdf5datahandler_ohlcv_load_and_resave(
|
|||||||
def test_generic_datahandler_ohlcv_load_and_resave(
|
def test_generic_datahandler_ohlcv_load_and_resave(
|
||||||
datahandler,
|
datahandler,
|
||||||
testdatadir,
|
testdatadir,
|
||||||
tmpdir,
|
tmp_path,
|
||||||
pair,
|
pair,
|
||||||
timeframe,
|
timeframe,
|
||||||
candle_type,
|
candle_type,
|
||||||
candle_append,
|
candle_append,
|
||||||
startdt, enddt
|
startdt, enddt
|
||||||
):
|
):
|
||||||
tmpdir1 = Path(tmpdir)
|
tmpdir2 = tmp_path
|
||||||
tmpdir2 = tmpdir1
|
|
||||||
if candle_type not in ('', 'spot'):
|
if candle_type not in ('', 'spot'):
|
||||||
tmpdir2 = tmpdir1 / 'futures'
|
tmpdir2 = tmp_path / 'futures'
|
||||||
tmpdir2.mkdir()
|
tmpdir2.mkdir()
|
||||||
# Load data from one common file
|
# Load data from one common file
|
||||||
dhbase = get_datahandler(testdatadir, 'feather')
|
dhbase = get_datahandler(testdatadir, 'feather')
|
||||||
@@ -403,7 +401,7 @@ def test_generic_datahandler_ohlcv_load_and_resave(
|
|||||||
file = tmpdir2 / f"UNITTEST_NEW-{timeframe}{candle_append}.{dh._get_file_extension()}"
|
file = tmpdir2 / f"UNITTEST_NEW-{timeframe}{candle_append}.{dh._get_file_extension()}"
|
||||||
assert not file.is_file()
|
assert not file.is_file()
|
||||||
|
|
||||||
dh1 = get_datahandler(tmpdir1, datahandler)
|
dh1 = get_datahandler(tmp_path, datahandler)
|
||||||
dh1.ohlcv_store('UNITTEST/NEW', timeframe, ohlcv, candle_type=candle_type)
|
dh1.ohlcv_store('UNITTEST/NEW', timeframe, ohlcv, candle_type=candle_type)
|
||||||
assert file.is_file()
|
assert file.is_file()
|
||||||
|
|
||||||
@@ -459,15 +457,14 @@ def test_datahandler_trades_load(testdatadir, datahandler):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('datahandler', ['jsongz', 'hdf5', 'feather', 'parquet'])
|
@pytest.mark.parametrize('datahandler', ['jsongz', 'hdf5', 'feather', 'parquet'])
|
||||||
def test_datahandler_trades_store(testdatadir, tmpdir, datahandler):
|
def test_datahandler_trades_store(testdatadir, tmp_path, datahandler):
|
||||||
tmpdir1 = Path(tmpdir)
|
|
||||||
dh = get_datahandler(testdatadir, datahandler)
|
dh = get_datahandler(testdatadir, datahandler)
|
||||||
trades = dh.trades_load('XRP/ETH')
|
trades = dh.trades_load('XRP/ETH')
|
||||||
|
|
||||||
dh1 = get_datahandler(tmpdir1, datahandler)
|
dh1 = get_datahandler(tmp_path, datahandler)
|
||||||
dh1.trades_store('XRP/NEW', trades)
|
dh1.trades_store('XRP/NEW', trades)
|
||||||
|
|
||||||
file = tmpdir1 / f'XRP_NEW-trades.{dh1._get_file_extension()}'
|
file = tmp_path / f'XRP_NEW-trades.{dh1._get_file_extension()}'
|
||||||
assert file.is_file()
|
assert file.is_file()
|
||||||
# Load trades back
|
# Load trades back
|
||||||
trades_new = dh1.trades_load('XRP/NEW')
|
trades_new = dh1.trades_load('XRP/NEW')
|
||||||
@@ -516,11 +513,11 @@ def test_gethandlerclass():
|
|||||||
|
|
||||||
def test_get_datahandler(testdatadir):
|
def test_get_datahandler(testdatadir):
|
||||||
dh = get_datahandler(testdatadir, 'json')
|
dh = get_datahandler(testdatadir, 'json')
|
||||||
assert type(dh) == JsonDataHandler
|
assert isinstance(dh, JsonDataHandler)
|
||||||
dh = get_datahandler(testdatadir, 'jsongz')
|
dh = get_datahandler(testdatadir, 'jsongz')
|
||||||
assert type(dh) == JsonGzDataHandler
|
assert isinstance(dh, JsonGzDataHandler)
|
||||||
dh1 = get_datahandler(testdatadir, 'jsongz', dh)
|
dh1 = get_datahandler(testdatadir, 'jsongz', dh)
|
||||||
assert id(dh1) == id(dh)
|
assert id(dh1) == id(dh)
|
||||||
|
|
||||||
dh = get_datahandler(testdatadir, 'hdf5')
|
dh = get_datahandler(testdatadir, 'hdf5')
|
||||||
assert type(dh) == HDF5DataHandler
|
assert isinstance(dh, HDF5DataHandler)
|
||||||
|
|||||||
@@ -500,3 +500,89 @@ def test_dp__add_external_df(default_conf_usdt):
|
|||||||
# 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00
|
# 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00
|
||||||
assert isinstance(res[1], int)
|
assert isinstance(res[1], int)
|
||||||
assert res[1] == 0
|
assert res[1] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_dp_get_required_startup(default_conf_usdt):
|
||||||
|
timeframe = '1h'
|
||||||
|
default_conf_usdt["timeframe"] = timeframe
|
||||||
|
dp = DataProvider(default_conf_usdt, None)
|
||||||
|
|
||||||
|
# No FreqAI config
|
||||||
|
assert dp.get_required_startup('5m', False) == 0
|
||||||
|
assert dp.get_required_startup('1h', False) == 0
|
||||||
|
assert dp.get_required_startup('1d', False) == 0
|
||||||
|
assert dp.get_required_startup('1d', True) == 0
|
||||||
|
assert dp.get_required_startup('1d') == 0
|
||||||
|
|
||||||
|
dp._config['startup_candle_count'] = 20
|
||||||
|
assert dp.get_required_startup('5m', False) == 20
|
||||||
|
assert dp.get_required_startup('5m', True) == 20
|
||||||
|
assert dp.get_required_startup('1h', False) == 20
|
||||||
|
assert dp.get_required_startup('1h') == 20
|
||||||
|
|
||||||
|
# With freqAI config
|
||||||
|
|
||||||
|
dp._config['freqai'] = {
|
||||||
|
'enabled': True,
|
||||||
|
'train_period_days': 20,
|
||||||
|
'feature_parameters': {
|
||||||
|
'indicator_periods_candles': [
|
||||||
|
5,
|
||||||
|
20,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert dp.get_required_startup('5m', False) == 20
|
||||||
|
assert dp.get_required_startup('5m', True) == 5780
|
||||||
|
|
||||||
|
assert dp.get_required_startup('1h', False) == 20
|
||||||
|
assert dp.get_required_startup('1h', True) == 500
|
||||||
|
|
||||||
|
assert dp.get_required_startup('1d', False) == 20
|
||||||
|
assert dp.get_required_startup('1d', True) == 40
|
||||||
|
assert dp.get_required_startup('1d') == 40
|
||||||
|
|
||||||
|
# FreqAI kindof ignores startup_candle_count if it's below indicator_periods_candles
|
||||||
|
dp._config['startup_candle_count'] = 0
|
||||||
|
assert dp.get_required_startup('5m', False) == 20
|
||||||
|
assert dp.get_required_startup('5m', True) == 5780
|
||||||
|
|
||||||
|
assert dp.get_required_startup('1h', False) == 20
|
||||||
|
assert dp.get_required_startup('1h', True) == 500
|
||||||
|
|
||||||
|
assert dp.get_required_startup('1d', False) == 20
|
||||||
|
assert dp.get_required_startup('1d', True) == 40
|
||||||
|
assert dp.get_required_startup('1d') == 40
|
||||||
|
|
||||||
|
dp._config['freqai']['feature_parameters']['indicator_periods_candles'][1] = 50
|
||||||
|
assert dp.get_required_startup('5m', False) == 50
|
||||||
|
assert dp.get_required_startup('5m', True) == 5810
|
||||||
|
|
||||||
|
assert dp.get_required_startup('1h', False) == 50
|
||||||
|
assert dp.get_required_startup('1h', True) == 530
|
||||||
|
|
||||||
|
assert dp.get_required_startup('1d', False) == 50
|
||||||
|
assert dp.get_required_startup('1d', True) == 70
|
||||||
|
assert dp.get_required_startup('1d') == 70
|
||||||
|
|
||||||
|
# scenario from issue https://github.com/freqtrade/freqtrade/issues/9432
|
||||||
|
dp._config['freqai'] = {
|
||||||
|
'enabled': True,
|
||||||
|
'train_period_days': 180,
|
||||||
|
'feature_parameters': {
|
||||||
|
'indicator_periods_candles': [
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dp._config['startup_candle_count'] = 40
|
||||||
|
assert dp.get_required_startup('5m', False) == 40
|
||||||
|
assert dp.get_required_startup('5m', True) == 51880
|
||||||
|
|
||||||
|
assert dp.get_required_startup('1h', False) == 40
|
||||||
|
assert dp.get_required_startup('1h', True) == 4360
|
||||||
|
|
||||||
|
assert dp.get_required_startup('1d', False) == 40
|
||||||
|
assert dp.get_required_startup('1d', True) == 220
|
||||||
|
assert dp.get_required_startup('1d') == 220
|
||||||
|
|||||||
+20
-24
@@ -106,17 +106,16 @@ def test_load_data_startup_candles(mocker, testdatadir) -> None:
|
|||||||
|
|
||||||
@pytest.mark.parametrize('candle_type', ['mark', ''])
|
@pytest.mark.parametrize('candle_type', ['mark', ''])
|
||||||
def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog,
|
def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog,
|
||||||
default_conf, tmpdir, candle_type) -> None:
|
default_conf, tmp_path, candle_type) -> None:
|
||||||
"""
|
"""
|
||||||
Test load_pair_history() with 1 min timeframe
|
Test load_pair_history() with 1 min timeframe
|
||||||
"""
|
"""
|
||||||
tmpdir1 = Path(tmpdir)
|
|
||||||
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list)
|
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
file = tmpdir1 / 'MEME_BTC-1m.feather'
|
file = tmp_path / 'MEME_BTC-1m.feather'
|
||||||
|
|
||||||
# do not download a new pair if refresh_pairs isn't set
|
# do not download a new pair if refresh_pairs isn't set
|
||||||
load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC', candle_type=candle_type)
|
load_pair_history(datadir=tmp_path, timeframe='1m', pair='MEME/BTC', candle_type=candle_type)
|
||||||
assert not file.is_file()
|
assert not file.is_file()
|
||||||
assert log_has(
|
assert log_has(
|
||||||
f"No history for MEME/BTC, {candle_type}, 1m found. "
|
f"No history for MEME/BTC, {candle_type}, 1m found. "
|
||||||
@@ -124,10 +123,10 @@ def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog,
|
|||||||
)
|
)
|
||||||
|
|
||||||
# download a new pair if refresh_pairs is set
|
# download a new pair if refresh_pairs is set
|
||||||
refresh_data(datadir=tmpdir1, timeframe='1m', pairs=['MEME/BTC'],
|
refresh_data(datadir=tmp_path, timeframe='1m', pairs=['MEME/BTC'],
|
||||||
exchange=exchange, candle_type=CandleType.SPOT
|
exchange=exchange, candle_type=CandleType.SPOT
|
||||||
)
|
)
|
||||||
load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC', candle_type=candle_type)
|
load_pair_history(datadir=tmp_path, timeframe='1m', pair='MEME/BTC', candle_type=candle_type)
|
||||||
assert file.is_file()
|
assert file.is_file()
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
r'\(0/1\) - Download history data for "MEME/BTC", 1m, '
|
r'\(0/1\) - Download history data for "MEME/BTC", 1m, '
|
||||||
@@ -273,27 +272,26 @@ def test_download_pair_history(
|
|||||||
ohlcv_history_list,
|
ohlcv_history_list,
|
||||||
mocker,
|
mocker,
|
||||||
default_conf,
|
default_conf,
|
||||||
tmpdir,
|
tmp_path,
|
||||||
candle_type,
|
candle_type,
|
||||||
subdir,
|
subdir,
|
||||||
file_tail
|
file_tail
|
||||||
) -> None:
|
) -> None:
|
||||||
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list)
|
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
tmpdir1 = Path(tmpdir)
|
file1_1 = tmp_path / f'{subdir}MEME_BTC-1m{file_tail}.feather'
|
||||||
file1_1 = tmpdir1 / f'{subdir}MEME_BTC-1m{file_tail}.feather'
|
file1_5 = tmp_path / f'{subdir}MEME_BTC-5m{file_tail}.feather'
|
||||||
file1_5 = tmpdir1 / f'{subdir}MEME_BTC-5m{file_tail}.feather'
|
file2_1 = tmp_path / f'{subdir}CFI_BTC-1m{file_tail}.feather'
|
||||||
file2_1 = tmpdir1 / f'{subdir}CFI_BTC-1m{file_tail}.feather'
|
file2_5 = tmp_path / f'{subdir}CFI_BTC-5m{file_tail}.feather'
|
||||||
file2_5 = tmpdir1 / f'{subdir}CFI_BTC-5m{file_tail}.feather'
|
|
||||||
|
|
||||||
assert not file1_1.is_file()
|
assert not file1_1.is_file()
|
||||||
assert not file2_1.is_file()
|
assert not file2_1.is_file()
|
||||||
|
|
||||||
assert _download_pair_history(datadir=tmpdir1, exchange=exchange,
|
assert _download_pair_history(datadir=tmp_path, exchange=exchange,
|
||||||
pair='MEME/BTC',
|
pair='MEME/BTC',
|
||||||
timeframe='1m',
|
timeframe='1m',
|
||||||
candle_type=candle_type)
|
candle_type=candle_type)
|
||||||
assert _download_pair_history(datadir=tmpdir1, exchange=exchange,
|
assert _download_pair_history(datadir=tmp_path, exchange=exchange,
|
||||||
pair='CFI/BTC',
|
pair='CFI/BTC',
|
||||||
timeframe='1m',
|
timeframe='1m',
|
||||||
candle_type=candle_type)
|
candle_type=candle_type)
|
||||||
@@ -308,11 +306,11 @@ def test_download_pair_history(
|
|||||||
assert not file1_5.is_file()
|
assert not file1_5.is_file()
|
||||||
assert not file2_5.is_file()
|
assert not file2_5.is_file()
|
||||||
|
|
||||||
assert _download_pair_history(datadir=tmpdir1, exchange=exchange,
|
assert _download_pair_history(datadir=tmp_path, exchange=exchange,
|
||||||
pair='MEME/BTC',
|
pair='MEME/BTC',
|
||||||
timeframe='5m',
|
timeframe='5m',
|
||||||
candle_type=candle_type)
|
candle_type=candle_type)
|
||||||
assert _download_pair_history(datadir=tmpdir1, exchange=exchange,
|
assert _download_pair_history(datadir=tmp_path, exchange=exchange,
|
||||||
pair='CFI/BTC',
|
pair='CFI/BTC',
|
||||||
timeframe='5m',
|
timeframe='5m',
|
||||||
candle_type=candle_type)
|
candle_type=candle_type)
|
||||||
@@ -340,13 +338,12 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
|
|||||||
assert json_dump_mock.call_count == 3
|
assert json_dump_mock.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
def test_download_backtesting_data_exception(mocker, caplog, default_conf, tmpdir) -> None:
|
def test_download_backtesting_data_exception(mocker, caplog, default_conf, tmp_path) -> None:
|
||||||
mocker.patch(f'{EXMS}.get_historic_ohlcv',
|
mocker.patch(f'{EXMS}.get_historic_ohlcv',
|
||||||
side_effect=Exception('File Error'))
|
side_effect=Exception('File Error'))
|
||||||
tmpdir1 = Path(tmpdir)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
assert not _download_pair_history(datadir=tmpdir1, exchange=exchange,
|
assert not _download_pair_history(datadir=tmp_path, exchange=exchange,
|
||||||
pair='MEME/BTC',
|
pair='MEME/BTC',
|
||||||
timeframe='1m', candle_type='spot')
|
timeframe='1m', candle_type='spot')
|
||||||
assert log_has('Failed to download history data for pair: "MEME/BTC", timeframe: 1m.', caplog)
|
assert log_has('Failed to download history data for pair: "MEME/BTC", timeframe: 1m.', caplog)
|
||||||
@@ -570,16 +567,15 @@ def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, tes
|
|||||||
|
|
||||||
|
|
||||||
def test_download_trades_history(trades_history, mocker, default_conf, testdatadir, caplog,
|
def test_download_trades_history(trades_history, mocker, default_conf, testdatadir, caplog,
|
||||||
tmpdir, time_machine) -> None:
|
tmp_path, time_machine) -> None:
|
||||||
start_dt = dt_utc(2023, 1, 1)
|
start_dt = dt_utc(2023, 1, 1)
|
||||||
time_machine.move_to(start_dt, tick=False)
|
time_machine.move_to(start_dt, tick=False)
|
||||||
|
|
||||||
tmpdir1 = Path(tmpdir)
|
|
||||||
ght_mock = MagicMock(side_effect=lambda pair, *args, **kwargs: (pair, trades_history))
|
ght_mock = MagicMock(side_effect=lambda pair, *args, **kwargs: (pair, trades_history))
|
||||||
mocker.patch(f'{EXMS}.get_historic_trades', ght_mock)
|
mocker.patch(f'{EXMS}.get_historic_trades', ght_mock)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
file1 = tmpdir1 / 'ETH_BTC-trades.json.gz'
|
file1 = tmp_path / 'ETH_BTC-trades.json.gz'
|
||||||
data_handler = get_datahandler(tmpdir1, data_format='jsongz')
|
data_handler = get_datahandler(tmp_path, data_format='jsongz')
|
||||||
|
|
||||||
assert not file1.is_file()
|
assert not file1.is_file()
|
||||||
|
|
||||||
@@ -614,7 +610,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
|
|||||||
pair='ETH/BTC')
|
pair='ETH/BTC')
|
||||||
assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog)
|
assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog)
|
||||||
|
|
||||||
file2 = tmpdir1 / 'XRP_ETH-trades.json.gz'
|
file2 = tmp_path / 'XRP_ETH-trades.json.gz'
|
||||||
copyfile(testdatadir / file2.name, file2)
|
copyfile(testdatadir / file2.name, file2)
|
||||||
|
|
||||||
ght_mock.reset_mock()
|
ght_mock.reset_mock()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
|
||||||
from shutil import copytree
|
from shutil import copytree
|
||||||
from unittest.mock import PropertyMock
|
from unittest.mock import PropertyMock
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ from freqtrade.exceptions import OperationalException
|
|||||||
from tests.conftest import EXMS, log_has, log_has_re, patch_exchange
|
from tests.conftest import EXMS, log_has, log_has_re, patch_exchange
|
||||||
|
|
||||||
|
|
||||||
def test_import_kraken_trades_from_csv(testdatadir, tmpdir, caplog, default_conf_usdt, mocker):
|
def test_import_kraken_trades_from_csv(testdatadir, tmp_path, caplog, default_conf_usdt, mocker):
|
||||||
with pytest.raises(OperationalException, match="This function is only for the kraken exchange"):
|
with pytest.raises(OperationalException, match="This function is only for the kraken exchange"):
|
||||||
import_kraken_trades_from_csv(default_conf_usdt, 'feather')
|
import_kraken_trades_from_csv(default_conf_usdt, 'feather')
|
||||||
|
|
||||||
@@ -21,10 +20,9 @@ def test_import_kraken_trades_from_csv(testdatadir, tmpdir, caplog, default_conf
|
|||||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={
|
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={
|
||||||
'BCH/EUR': {'symbol': 'BCH/EUR', 'id': 'BCHEUR', 'altname': 'BCHEUR'},
|
'BCH/EUR': {'symbol': 'BCH/EUR', 'id': 'BCHEUR', 'altname': 'BCHEUR'},
|
||||||
}))
|
}))
|
||||||
tmpdir1 = Path(tmpdir)
|
dstfile = tmp_path / 'BCH_EUR-trades.feather'
|
||||||
dstfile = tmpdir1 / 'BCH_EUR-trades.feather'
|
|
||||||
assert not dstfile.is_file()
|
assert not dstfile.is_file()
|
||||||
default_conf_usdt['datadir'] = tmpdir1
|
default_conf_usdt['datadir'] = tmp_path
|
||||||
# There's 2 files in this tree, containing a total of 2 days.
|
# There's 2 files in this tree, containing a total of 2 days.
|
||||||
# tests/testdata/kraken/
|
# tests/testdata/kraken/
|
||||||
# └── trades_csv
|
# └── trades_csv
|
||||||
@@ -32,7 +30,7 @@ def test_import_kraken_trades_from_csv(testdatadir, tmpdir, caplog, default_conf
|
|||||||
# └── incremental_q2
|
# └── incremental_q2
|
||||||
# └── BCHEUR.csv <-- 2023-01-02
|
# └── BCHEUR.csv <-- 2023-01-02
|
||||||
|
|
||||||
copytree(testdatadir / 'kraken/trades_csv', tmpdir1 / 'trades_csv')
|
copytree(testdatadir / 'kraken/trades_csv', tmp_path / 'trades_csv')
|
||||||
|
|
||||||
import_kraken_trades_from_csv(default_conf_usdt, 'feather')
|
import_kraken_trades_from_csv(default_conf_usdt, 'feather')
|
||||||
assert log_has("Found csv files for BCHEUR.", caplog)
|
assert log_has("Found csv files for BCHEUR.", caplog)
|
||||||
@@ -40,7 +38,7 @@ def test_import_kraken_trades_from_csv(testdatadir, tmpdir, caplog, default_conf
|
|||||||
|
|
||||||
assert dstfile.is_file()
|
assert dstfile.is_file()
|
||||||
|
|
||||||
dh = get_datahandler(tmpdir1, 'feather')
|
dh = get_datahandler(tmp_path, 'feather')
|
||||||
trades = dh.trades_load('BCH_EUR')
|
trades = dh.trades_load('BCH_EUR')
|
||||||
assert len(trades) == 340
|
assert len(trades) == 340
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from freqtrade.enums import CandleType, MarginMode, TradingMode
|
|||||||
from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError,
|
from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError,
|
||||||
InsufficientFundsError, InvalidOrderException,
|
InsufficientFundsError, InvalidOrderException,
|
||||||
OperationalException, PricingError, TemporaryError)
|
OperationalException, PricingError, TemporaryError)
|
||||||
from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, market_is_active,
|
from freqtrade.exchange import (Binance, Bybit, Exchange, Kraken, market_is_active,
|
||||||
timeframe_to_prev_date)
|
timeframe_to_prev_date)
|
||||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
|
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
|
||||||
calculate_backoff, remove_exchange_credentials)
|
calculate_backoff, remove_exchange_credentials)
|
||||||
@@ -24,7 +24,7 @@ from tests.conftest import (EXMS, generate_test_data_raw, get_mock_coro, get_pat
|
|||||||
|
|
||||||
|
|
||||||
# Make sure to always keep one exchange here which is NOT subclassed!!
|
# Make sure to always keep one exchange here which is NOT subclassed!!
|
||||||
EXCHANGES = ['bittrex', 'binance', 'kraken', 'gate', 'kucoin', 'bybit', 'okx']
|
EXCHANGES = ['binance', 'kraken', 'gate', 'kucoin', 'bybit', 'okx']
|
||||||
|
|
||||||
get_entry_rate_data = [
|
get_entry_rate_data = [
|
||||||
('other', 20, 19, 10, 0.0, 20), # Full ask side
|
('other', 20, 19, 10, 0.0, 20), # Full ask side
|
||||||
@@ -228,10 +228,10 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
|||||||
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
default_conf['exchange']['name'] = 'Bittrex'
|
default_conf['exchange']['name'] = 'Bybit'
|
||||||
exchange = ExchangeResolver.load_exchange(default_conf)
|
exchange = ExchangeResolver.load_exchange(default_conf)
|
||||||
assert isinstance(exchange, Exchange)
|
assert isinstance(exchange, Exchange)
|
||||||
assert isinstance(exchange, Bittrex)
|
assert isinstance(exchange, Bybit)
|
||||||
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
|
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
|
||||||
caplog)
|
caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
@@ -263,8 +263,8 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
|||||||
|
|
||||||
def test_validate_order_time_in_force(default_conf, mocker, caplog):
|
def test_validate_order_time_in_force(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
# explicitly test bittrex, exchanges implementing other policies need separate tests
|
# explicitly test bybit, exchanges implementing other policies need separate tests
|
||||||
ex = get_patched_exchange(mocker, default_conf, id="bittrex")
|
ex = get_patched_exchange(mocker, default_conf, id="bybit")
|
||||||
tif = {
|
tif = {
|
||||||
"buy": "gtc",
|
"buy": "gtc",
|
||||||
"sell": "gtc",
|
"sell": "gtc",
|
||||||
@@ -273,11 +273,14 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
|
|||||||
ex.validate_order_time_in_force(tif)
|
ex.validate_order_time_in_force(tif)
|
||||||
tif2 = {
|
tif2 = {
|
||||||
"buy": "fok",
|
"buy": "fok",
|
||||||
"sell": "ioc",
|
"sell": "ioc22",
|
||||||
}
|
}
|
||||||
with pytest.raises(OperationalException, match=r"Time in force.*not supported for .*"):
|
with pytest.raises(OperationalException, match=r"Time in force.*not supported for .*"):
|
||||||
ex.validate_order_time_in_force(tif2)
|
ex.validate_order_time_in_force(tif2)
|
||||||
|
tif2 = {
|
||||||
|
"buy": "fok",
|
||||||
|
"sell": "ioc",
|
||||||
|
}
|
||||||
# Patch to see if this will pass if the values are in the ft dict
|
# Patch to see if this will pass if the values are in the ft dict
|
||||||
ex._ft_has.update({"order_time_in_force": ["GTC", "FOK", "IOC"]})
|
ex._ft_has.update({"order_time_in_force": ["GTC", "FOK", "IOC"]})
|
||||||
ex.validate_order_time_in_force(tif2)
|
ex.validate_order_time_in_force(tif2)
|
||||||
@@ -915,7 +918,6 @@ def test_validate_ordertypes(default_conf, mocker):
|
|||||||
mocker.patch(f'{EXMS}.validate_timeframes')
|
mocker.patch(f'{EXMS}.validate_timeframes')
|
||||||
mocker.patch(f'{EXMS}.validate_stakecurrency')
|
mocker.patch(f'{EXMS}.validate_stakecurrency')
|
||||||
mocker.patch(f'{EXMS}.validate_pricing')
|
mocker.patch(f'{EXMS}.validate_pricing')
|
||||||
mocker.patch(f'{EXMS}.name', 'Bittrex')
|
|
||||||
|
|
||||||
default_conf['order_types'] = {
|
default_conf['order_types'] = {
|
||||||
'entry': 'limit',
|
'entry': 'limit',
|
||||||
@@ -1851,7 +1853,7 @@ def test_fetch_bids_asks(default_conf, mocker):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_get_tickers(default_conf, mocker, exchange_name):
|
def test_get_tickers(default_conf, mocker, exchange_name, caplog):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
tick = {'ETH/BTC': {
|
tick = {'ETH/BTC': {
|
||||||
'symbol': 'ETH/BTC',
|
'symbol': 'ETH/BTC',
|
||||||
@@ -1900,6 +1902,14 @@ def test_get_tickers(default_conf, mocker, exchange_name):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
exchange.get_tickers()
|
exchange.get_tickers()
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
|
api_mock.fetch_tickers = MagicMock(side_effect=[ccxt.BadSymbol("SomeSymbol"), []])
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
x = exchange.get_tickers()
|
||||||
|
assert x == []
|
||||||
|
assert log_has_re(r'Could not load tickers due to BadSymbol\..*SomeSymbol', caplog)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
api_mock.fetch_tickers = MagicMock(return_value={})
|
api_mock.fetch_tickers = MagicMock(return_value={})
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
exchange.get_tickers()
|
exchange.get_tickers()
|
||||||
@@ -1969,6 +1979,34 @@ def test_fetch_ticker(default_conf, mocker, exchange_name):
|
|||||||
exchange.fetch_ticker(pair='XRP/ETH')
|
exchange.fetch_ticker(pair='XRP/ETH')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
|
def test___now_is_time_to_refresh(default_conf, mocker, exchange_name, time_machine):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
|
pair = 'BTC/USDT'
|
||||||
|
candle_type = CandleType.SPOT
|
||||||
|
start_dt = datetime(2023, 12, 1, 0, 10, 0, tzinfo=timezone.utc)
|
||||||
|
time_machine.move_to(start_dt, tick=False)
|
||||||
|
assert (pair, '5m', candle_type) not in exchange._pairs_last_refresh_time
|
||||||
|
|
||||||
|
# not refreshed yet
|
||||||
|
assert exchange._now_is_time_to_refresh(pair, '5m', candle_type) is True
|
||||||
|
|
||||||
|
last_closed_candle = (start_dt - timedelta(minutes=5)).timestamp()
|
||||||
|
exchange._pairs_last_refresh_time[(pair, '5m', candle_type)] = last_closed_candle
|
||||||
|
|
||||||
|
# next candle not closed yet
|
||||||
|
time_machine.move_to(start_dt + timedelta(minutes=4, seconds=59), tick=False)
|
||||||
|
assert exchange._now_is_time_to_refresh(pair, '5m', candle_type) is False
|
||||||
|
|
||||||
|
# next candle closed
|
||||||
|
time_machine.move_to(start_dt + timedelta(minutes=5, seconds=0), tick=False)
|
||||||
|
assert exchange._now_is_time_to_refresh(pair, '5m', candle_type) is True
|
||||||
|
|
||||||
|
# 1 second later (last_refresh_time didn't change)
|
||||||
|
time_machine.move_to(start_dt + timedelta(minutes=5, seconds=1), tick=False)
|
||||||
|
assert exchange._now_is_time_to_refresh(pair, '5m', candle_type) is True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
@pytest.mark.parametrize('candle_type', ['mark', ''])
|
@pytest.mark.parametrize('candle_type', ['mark', ''])
|
||||||
def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_type):
|
def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_type):
|
||||||
@@ -2730,7 +2768,6 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
|
|||||||
assert res_ohlcv[9][4] == 0.07668
|
assert res_ohlcv[9][4] == 0.07668
|
||||||
assert res_ohlcv[9][5] == 16.65244264
|
assert res_ohlcv[9][5] == 16.65244264
|
||||||
|
|
||||||
# Bittrex use-case (real data from Bittrex)
|
|
||||||
# This OHLCV data is ordered ASC (oldest first, newest last)
|
# This OHLCV data is ordered ASC (oldest first, newest last)
|
||||||
ohlcv = [
|
ohlcv = [
|
||||||
[1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924],
|
[1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924],
|
||||||
@@ -3374,7 +3411,7 @@ def test_get_fee(default_conf, mocker, exchange_name):
|
|||||||
|
|
||||||
|
|
||||||
def test_stoploss_order_unsupported_exchange(default_conf, mocker):
|
def test_stoploss_order_unsupported_exchange(default_conf, mocker):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id='bittrex')
|
exchange = get_patched_exchange(mocker, default_conf, id='bitpanda')
|
||||||
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
|
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
|
||||||
exchange.create_stoploss(
|
exchange.create_stoploss(
|
||||||
pair='ETH/BTC',
|
pair='ETH/BTC',
|
||||||
@@ -3570,10 +3607,10 @@ def test_ohlcv_candle_limit(default_conf, mocker, exchange_name):
|
|||||||
timeframes = ('1m', '5m', '1h')
|
timeframes = ('1m', '5m', '1h')
|
||||||
expected = exchange._ft_has['ohlcv_candle_limit']
|
expected = exchange._ft_has['ohlcv_candle_limit']
|
||||||
for timeframe in timeframes:
|
for timeframe in timeframes:
|
||||||
if 'ohlcv_candle_limit_per_timeframe' in exchange._ft_has:
|
# if 'ohlcv_candle_limit_per_timeframe' in exchange._ft_has:
|
||||||
expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe]
|
# expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe]
|
||||||
# This should only run for bittrex
|
# This should only run for bittrex
|
||||||
assert exchange_name == 'bittrex'
|
# assert exchange_name == 'bittrex'
|
||||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == expected
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == expected
|
||||||
|
|
||||||
|
|
||||||
@@ -3865,11 +3902,11 @@ def test_set_margin_mode(mocker, default_conf, margin_mode):
|
|||||||
("kraken", TradingMode.SPOT, None, False),
|
("kraken", TradingMode.SPOT, None, False),
|
||||||
("kraken", TradingMode.MARGIN, MarginMode.ISOLATED, True),
|
("kraken", TradingMode.MARGIN, MarginMode.ISOLATED, True),
|
||||||
("kraken", TradingMode.FUTURES, MarginMode.ISOLATED, True),
|
("kraken", TradingMode.FUTURES, MarginMode.ISOLATED, True),
|
||||||
("bittrex", TradingMode.SPOT, None, False),
|
("bitmart", TradingMode.SPOT, None, False),
|
||||||
("bittrex", TradingMode.MARGIN, MarginMode.CROSS, True),
|
("bitmart", TradingMode.MARGIN, MarginMode.CROSS, True),
|
||||||
("bittrex", TradingMode.MARGIN, MarginMode.ISOLATED, True),
|
("bitmart", TradingMode.MARGIN, MarginMode.ISOLATED, True),
|
||||||
("bittrex", TradingMode.FUTURES, MarginMode.CROSS, True),
|
("bitmart", TradingMode.FUTURES, MarginMode.CROSS, True),
|
||||||
("bittrex", TradingMode.FUTURES, MarginMode.ISOLATED, True),
|
("bitmart", TradingMode.FUTURES, MarginMode.ISOLATED, True),
|
||||||
("gate", TradingMode.MARGIN, MarginMode.ISOLATED, True),
|
("gate", TradingMode.MARGIN, MarginMode.ISOLATED, True),
|
||||||
("okx", TradingMode.SPOT, None, False),
|
("okx", TradingMode.SPOT, None, False),
|
||||||
("okx", TradingMode.MARGIN, MarginMode.CROSS, True),
|
("okx", TradingMode.MARGIN, MarginMode.CROSS, True),
|
||||||
@@ -4486,10 +4523,10 @@ def test_amount_to_contract_precision(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('exchange_name,open_rate,is_short,trading_mode,margin_mode', [
|
@pytest.mark.parametrize('exchange_name,open_rate,is_short,trading_mode,margin_mode', [
|
||||||
# Bittrex
|
# Bybit
|
||||||
('bittrex', 2.0, False, 'spot', None),
|
('bybit', 2.0, False, 'spot', None),
|
||||||
('bittrex', 2.0, False, 'spot', 'cross'),
|
('bybit', 2.0, False, 'spot', 'cross'),
|
||||||
('bittrex', 2.0, True, 'spot', 'isolated'),
|
('bybit', 2.0, True, 'spot', 'isolated'),
|
||||||
# Binance
|
# Binance
|
||||||
('binance', 2.0, False, 'spot', None),
|
('binance', 2.0, False, 'spot', None),
|
||||||
('binance', 2.0, False, 'spot', 'cross'),
|
('binance', 2.0, False, 'spot', 'cross'),
|
||||||
@@ -4911,7 +4948,7 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers):
|
|||||||
exchange.get_max_leverage("BTC/USDT:USDT", 1000000000.01)
|
exchange.get_max_leverage("BTC/USDT:USDT", 1000000000.01)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'gate', 'okx', 'bybit'])
|
@pytest.mark.parametrize("exchange_name", ['binance', 'kraken', 'gate', 'okx', 'bybit'])
|
||||||
def test__get_params(mocker, default_conf, exchange_name):
|
def test__get_params(mocker, default_conf, exchange_name):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
|
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from tests.conftest import log_has_re
|
|||||||
def test_check_exchange(default_conf, caplog) -> None:
|
def test_check_exchange(default_conf, caplog) -> None:
|
||||||
# Test an officially supported by Freqtrade team exchange
|
# Test an officially supported by Freqtrade team exchange
|
||||||
default_conf['runmode'] = RunMode.DRY_RUN
|
default_conf['runmode'] = RunMode.DRY_RUN
|
||||||
default_conf.get('exchange').update({'name': 'BITTREX'})
|
default_conf.get('exchange').update({'name': 'BINANCE'})
|
||||||
assert check_exchange(default_conf)
|
assert check_exchange(default_conf)
|
||||||
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
||||||
caplog)
|
caplog)
|
||||||
@@ -41,14 +41,14 @@ def test_check_exchange(default_conf, caplog) -> None:
|
|||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
# Test an officially supported by Freqtrade team exchange - with remapping
|
# Test an officially supported by Freqtrade team exchange - with remapping
|
||||||
default_conf.get('exchange').update({'name': 'okex'})
|
default_conf.get('exchange').update({'name': 'okx'})
|
||||||
assert check_exchange(default_conf)
|
assert check_exchange(default_conf)
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
r"Exchange \"okex\" is officially supported by the Freqtrade development team\.",
|
r"Exchange \"okx\" is officially supported by the Freqtrade development team\.",
|
||||||
caplog)
|
caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
# Test an available exchange, supported by ccxt
|
# Test an available exchange, supported by ccxt
|
||||||
default_conf.get('exchange').update({'name': 'huobipro'})
|
default_conf.get('exchange').update({'name': 'huobijp'})
|
||||||
assert check_exchange(default_conf)
|
assert check_exchange(default_conf)
|
||||||
assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, "
|
assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, "
|
||||||
r"but not officially supported "
|
r"but not officially supported "
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user