Compare commits

...

320 Commits

Author SHA1 Message Date
Matthias
85c2ca0d03 Merge pull request #5034 from freqtrade/new_release
New release 2021.5
2021-05-27 14:52:44 +01:00
Matthias
639c83575b Fix csv-export error with new hyperopt format 2021-05-27 13:09:06 +02:00
Matthias
0c9b913cad Version bump 2021.5 2021-05-27 11:10:10 +02:00
Matthias
e42e06a593 Merge branch 'stable' into new_release 2021-05-27 11:10:00 +02:00
Matthias
f3d8e5c9e4 Improve hyperopt docs
closes #4949
2021-05-27 10:44:35 +02:00
Matthias
42453333be Align coinbase download with ccxt limits
Align with https://github.com/ccxt/ccxt/issues/9268
2021-05-27 07:38:47 +02:00
Matthias
8e89d3e6e4 Fix sort error 2021-05-25 19:33:34 +02:00
Matthias
cc5769e900 Convert np.int64 to proper int
closes #5018
2021-05-25 19:24:56 +02:00
Matthias
a747312c1e Explicitly provide is_open to trade Object
closes #5015
2021-05-25 18:02:07 +02:00
Matthias
aa4653549b Merge pull request #5019 from kamontat/patch-1
Wrong filename in devcontainer
2021-05-25 05:27:44 +01:00
Kamontat Chantrachirathumrong
7dcf94f80c Update Dockerfile 2021-05-25 08:18:14 +07:00
Matthias
9465fd390a Fix devcontainer 2021-05-24 17:01:53 +02:00
Matthias
c14a4eaa68 Merge pull request #5013 from nmenescardi/patch-2
Update strategy-advanced.md
2021-05-24 15:27:23 +01:00
Nicolas Menescardi
c0d3a31ddb Update strategy-advanced.md
fix some typos
2021-05-24 11:08:17 -03:00
Matthias
bb1222d9b8 Merge pull request #5009 from freqtrade/dependabot/pip/develop/ta-lib-0.4.20
Bump ta-lib from 0.4.19 to 0.4.20
2021-05-24 10:31:59 +01:00
Matthias
ba3997185b Update wheels for ta-lib 2021-05-24 10:43:48 +02:00
Matthias
84d8a4b061 Merge pull request #5010 from freqtrade/dependabot/pip/develop/ccxt-1.50.30
Bump ccxt from 1.50.6 to 1.50.30
2021-05-24 09:43:11 +01:00
Matthias
5d44ca0b82 Merge pull request #5005 from Pascal66/patch-2
Fix a rare error during hyperopt
2021-05-24 09:40:29 +01:00
Matthias
d95c526242 Merge pull request #5008 from freqtrade/dependabot/pip/develop/jinja2-3.0.1
Bump jinja2 from 3.0.0 to 3.0.1
2021-05-24 07:05:34 +01:00
Matthias
af16614bf2 Fix formatting issue 2021-05-24 07:48:36 +02:00
Matthias
929e085910 Merge pull request #5007 from freqtrade/dependabot/pip/develop/mkdocs-material-7.1.5
Bump mkdocs-material from 7.1.4 to 7.1.5
2021-05-24 06:46:11 +01:00
dependabot[bot]
20ccda1699 Bump ccxt from 1.50.6 to 1.50.30
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.50.6 to 1.50.30.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst)
- [Commits](https://github.com/ccxt/ccxt/compare/1.50.6...1.50.30)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-24 05:24:17 +00:00
dependabot[bot]
7757c476fd Bump ta-lib from 0.4.19 to 0.4.20
Bumps [ta-lib](https://github.com/mrjbq7/ta-lib) from 0.4.19 to 0.4.20.
- [Release notes](https://github.com/mrjbq7/ta-lib/releases)
- [Changelog](https://github.com/mrjbq7/ta-lib/blob/master/CHANGELOG)
- [Commits](https://github.com/mrjbq7/ta-lib/compare/TA_Lib-0.4.19...TA_Lib-0.4.20)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-24 05:24:08 +00:00
dependabot[bot]
2fddb4ae43 Bump jinja2 from 3.0.0 to 3.0.1
Bumps [jinja2](https://github.com/pallets/jinja) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.0.0...3.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-24 05:24:02 +00:00
dependabot[bot]
4c02e6667f Bump mkdocs-material from 7.1.4 to 7.1.5
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.4 to 7.1.5.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.4...7.1.5)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-24 05:23:55 +00:00
Priveyes
6f990c5976 Fix a rare error in save_result : ValueError: Out of range float values are not JSON compliant
freqtrade/freqtrade/optimize/hyperopt.py", line 166, in _save_result
    rapidjson.dump(epoch, f, default=str, number_mode=rapidjson.NM_NATIVE)
ValueError: Out of range float values are not JSON compliant
2021-05-23 18:49:07 +02:00
Matthias
ae037b0ec1 Merge pull request #4746 from gmatheu/contribution/telegram_forcebuy_inline_keyboard
Telegram: forcebuy inline keyboard
2021-05-23 15:32:47 +01:00
Matthias
f760b4a789 Merge branch 'develop' into pr/gmatheu/4746 2021-05-23 16:03:11 +02:00
Matthias
77302ea178 Update documentation for forcebuy query 2021-05-23 16:01:49 +02:00
Matthias
971d5b2ecc Merge pull request #5002 from freqtrade/track_rejected_trades
Track rejected trades
2021-05-23 14:56:50 +01:00
Matthias
3f956441fc Properly format % of zero_duration_trades 2021-05-23 15:53:54 +02:00
Matthias
a39860e0de Add tests for rejected signals 2021-05-23 14:15:02 +02:00
Matthias
7f125315b0 Track Rejected Trades
closes #3423
2021-05-23 09:42:05 +02:00
Matthias
02faeb60a3 Merge pull request #4943 from rokups/rk/statistics
Extra statistics
2021-05-23 08:38:27 +01:00
Matthias
916ece6a29 More realistic testcase for results 2021-05-23 09:15:36 +02:00
Rokas Kupstys
db985cbc2e Fix hyperopt-show failing to display old results with missing new fields. 2021-05-23 09:45:26 +03:00
Matthias
74d75599a9 Merge pull request #4964 from thraizz/develop
Add backoff timer for coingecko API
2021-05-22 16:54:29 +01:00
Matthias
765c824bfc isort 2021-05-22 17:25:23 +02:00
Matthias
a7bd8b0aa5 Fix exception in plotting when no trades where generated
as seen in #4981
2021-05-22 17:03:16 +02:00
Matthias
feb836eaf6 Merge pull request #4994 from freqtrade/support_other_dbs
Support other dbs
2021-05-22 15:54:34 +01:00
A. Schueler
0693458507 Update freqtrade/rpc/fiat_convert.py 2021-05-22 16:26:58 +02:00
Matthias
08c707e0cf Update docs with new format 2021-05-22 15:38:13 +02:00
Rokas Kupstys
25cc4eae96 Fix tests that broke after table formatting changed. 2021-05-22 15:25:37 +02:00
Matthias
5191c869c9 Merge pull request #4996 from JoeSchr/patch-5
Fix missing `not` in `empty` check
2021-05-22 13:57:17 +01:00
A. Schueler
be13856171 Fix flake8 error in test_fiat_convert 2021-05-22 13:43:48 +02:00
A. Schueler
f8cdd6475c Reduce warnings when waiting for coingecko backoff 2021-05-22 13:43:33 +02:00
JoeSchr
21d986710d Fix missing not in empty check
See discussing here: https://github.com/freqtrade/freqtrade/pull/4963#discussion_r633457596
seems that request was only partially implemented
2021-05-22 13:26:59 +02:00
A. Schueler
e4ca944597 Add tests for coingecko backoff 2021-05-22 12:04:24 +02:00
A. Schueler
6e05f856b4 Abort _find_price when cryptomap is empty after retry 2021-05-22 11:55:03 +02:00
A. Schueler
ab6bfbad12 Handle RequestExceptions that are not 429s in _load_cryptomap 2021-05-22 11:52:40 +02:00
A. Schueler
8842e0d161 Fix flake8 error in fiat_convert 2021-05-22 10:59:33 +02:00
A. Schueler
a0921ec753 Add backoff timer for coingecko API
Set a future timestamp when we should retry getting coingecko data.

This fixes conversion from stake to fiat when running multiple bots
as we don't simply accept the 429 error from Coingecko but handle it.
2021-05-22 10:59:33 +02:00
Matthias
df0928c8b5 Merge pull request #4992 from freqtrade/buy_partial_fill
Don't cancel low partial filled buy
2021-05-22 09:17:04 +01:00
Matthias
cc064f1574 String columns should have a max-length defined
otherwise MySql will not work.
2021-05-22 10:12:23 +02:00
Matthias
5285cd69b4 Add documentation for Postgres and Mysql 2021-05-22 10:12:03 +02:00
Matthias
44bbc0718e CLosing bracket 2021-05-21 20:54:18 +02:00
Matthias
a7216e6279 SQLite does not know drop column 2021-05-21 20:53:38 +02:00
Matthias
41e3233bab Fix failing test 2021-05-21 20:44:11 +02:00
Matthias
6acb2eb2b6 Add average column to orders table 2021-05-21 20:35:39 +02:00
Matthias
4e94d3d3e5 Add test for too small buy check 2021-05-21 19:49:36 +02:00
Matthias
45e2621505 Add minimum-filled protection for buy cancels 2021-05-21 19:32:16 +02:00
Matthias
4c08f0020a Merge pull request #4990 from nmenescardi/patch-1
Update strategy-customization.md
2021-05-21 16:03:53 +01:00
Nicolas Menescardi
9537d9f4e2 Update strategy-customization.md
Fix typo: 'This will method will...' -> 'This method will...'
2021-05-21 11:27:22 -03:00
Matthias
dfa412f0de Fix typo in filter 2021-05-21 13:24:13 +02:00
Rokas Kupstys
981b2df7ca Include win:loss ratio in results tables. 2021-05-21 12:18:08 +03:00
Rokas Kupstys
debd98ad9a Make results table more compact by merging win/draw/loss columns and drawdown abs/% into single columns. 2021-05-21 11:36:23 +03:00
Rokas Kupstys
e1dc1357ce Add drawdown column to strategy summary table. 2021-05-21 11:36:23 +03:00
Rokas Kupstys
edcfa94093 Include zero duration trades in backtesting report. 2021-05-21 11:36:23 +03:00
Matthias
0e6c1d28f4 Fix cleanup CI by updating action 2021-05-21 09:32:18 +02:00
Matthias
10b013dc34 Merge pull request #4963 from robcaulk/patch-1
update doc to reflect better empty dataframe check
2021-05-21 09:31:16 +02:00
Matthias
96ea10e562 Fix circular import in hyperopt 2021-05-21 08:52:56 +02:00
Matthias
f398888865 Refactor preprocessed trimming to seperate method 2021-05-21 08:26:19 +02:00
Matthias
d19b37c777 Merge pull request #4979 from kamontat/patch-1
Fix hyperopt trim to empty dataframe
2021-05-21 08:17:35 +02:00
Matthias
1a30e39222 Move squeeze into if block 2021-05-21 08:06:27 +02:00
Matthias
ccd705bfda Merge pull request #4962 from eschava/total_row
Total row for telegram "/status table" command
2021-05-21 08:02:30 +02:00
Matthias
e2edcb5457 Merge pull request #4988 from kamontat/patch-2
fix wrong json key
2021-05-21 07:19:48 +02:00
Kamontat Chantrachirathumrong
0045d3a726 fix wrong json key 2021-05-21 11:18:16 +07:00
Kamontat Chantrachirathumrong
6172e67fcd Update hyperopt.py 2021-05-20 11:56:31 +07:00
Kamontat Chantrachirathumrong
c2b9da68e1 fix indent 2021-05-20 11:56:11 +07:00
Kamontat Chantrachirathumrong
1b3bfb2e7f found root cause. 2021-05-20 11:50:15 +07:00
Kamontat Chantrachirathumrong
48210170e7 wrap with is not empty 2021-05-20 11:49:25 +07:00
Matthias
ba106e6c4a Merge pull request #4975 from Axel-CH/allow_seconds_unfilled_timeout
Add ability to choose unit in unfilled timeout
2021-05-20 06:43:15 +02:00
Matthias
586f2a699d Merge pull request #4953 from freqtrade/value_filter
max-value change filter
2021-05-20 06:35:34 +02:00
Matthias
0358b5365f Add "unfilledtimeout-unit" to full config sample 2021-05-20 06:26:07 +02:00
Kamontat Chantrachirathumrong
082fb11bbe Avoid having error cannot set a frame with no defined index and a scalar 2021-05-20 01:54:48 +07:00
Matthias
ef4d1c24d7 Merge pull request #4941 from brookmiles/fix-stoploss-above-candle
prevent backtest stoploss trade price being set above candle high
2021-05-19 06:20:35 +02:00
axel
75f88b466a add ability to choose unit in unfilled timeout 2021-05-18 19:30:36 -04:00
Matthias
7a9853bfe1 Fix "Too many open Files" exception 2021-05-18 20:39:55 +02:00
Matthias
2565f91bc2 Adjust tests to reflect new stoploss behaviour 2021-05-18 19:33:17 +02:00
Matthias
16c22c7b68 Add pair name to stoploss
helps debugging #4972
2021-05-18 19:16:25 +02:00
Matthias
36eba0f110 Don't use "r+" memmap, but "r2 2021-05-17 21:05:48 +02:00
Matthias
6aa574fa2b Convert ROI result to proper json object
closes #4952
2021-05-17 20:58:50 +02:00
Matthias
6659a07079 Add tests for max-value filter 2021-05-17 19:47:57 +02:00
Matthias
369f19df6b Add valuefilter to Pricefilters 2021-05-17 19:44:36 +02:00
Matthias
3006396398 Fix docstring typo 2021-05-17 19:37:22 +02:00
Matthias
26f5bc6584 Merge pull request #4944 from brookmiles/fix-download-data-end-date
Add support for timerange end date to `download-data`
2021-05-17 19:35:32 +02:00
Eugene Schava
459fae6d80 Total row for telegram "/status table" command
fixes
2021-05-17 16:22:48 +03:00
Eugene Schava
3ad8fa2f38 Total row for telegram "/status table" command
moved sum calculation to API
2021-05-17 15:59:03 +03:00
Matthias
c2bd1bf7e6 Merge pull request #4960 from freqtrade/dependabot/pip/develop/fastapi-0.65.1
Bump fastapi from 0.64.0 to 0.65.1
2021-05-17 14:31:30 +02:00
dependabot[bot]
cb50298bfe Bump fastapi from 0.64.0 to 0.65.1
Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.64.0 to 0.65.1.
- [Release notes](https://github.com/tiangolo/fastapi/releases)
- [Commits](https://github.com/tiangolo/fastapi/compare/0.64.0...0.65.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-17 12:05:13 +00:00
Matthias
4436700f5a Merge pull request #4959 from freqtrade/dependabot/pip/develop/aiofiles-0.7.0
Bump aiofiles from 0.6.0 to 0.7.0
2021-05-17 14:04:40 +02:00
Eugene Schava
196fde44e0 Total row for telegram "/status table" command
work around mypy warning
2021-05-17 14:45:54 +03:00
Matthias
748f5d6490 Merge pull request #4961 from freqtrade/dependabot/pip/develop/jinja2-3.0.0
Bump jinja2 from 2.11.3 to 3.0.0
2021-05-17 13:14:13 +02:00
Eugene Schava
915ff7e1bf Total row for telegram "/status table" command
fix mypy warnings
2021-05-17 13:03:20 +03:00
Eugene Schava
d7479fda1f Total row for telegram "/status table" command
fix compiler warnings
2021-05-17 12:53:57 +03:00
Eugene Schava
0abb9cfe28 Total row for telegram "/status table" command 2021-05-17 12:41:44 +03:00
Robert Caulk
860a4d2390 update doc to reflect better empty dataframe check 2021-05-17 11:40:57 +02:00
Matthias
6542070afa Merge pull request #4954 from freqtrade/dependabot/pip/develop/numpy-1.20.3
Bump numpy from 1.20.2 to 1.20.3
2021-05-17 10:41:27 +02:00
Eugene Schava
10ef0f54ac Total row for telegram "/status table" command 2021-05-17 11:12:11 +03:00
dependabot[bot]
40ae21f3a8 Bump numpy from 1.20.2 to 1.20.3
Bumps [numpy](https://github.com/numpy/numpy) from 1.20.2 to 1.20.3.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/HOWTO_RELEASE.rst.txt)
- [Commits](https://github.com/numpy/numpy/compare/v1.20.2...v1.20.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-17 07:59:36 +00:00
Matthias
c5d9c09220 Merge pull request #4958 from freqtrade/dependabot/pip/develop/sqlalchemy-1.4.15
Bump sqlalchemy from 1.4.14 to 1.4.15
2021-05-17 09:16:09 +02:00
Matthias
868706d132 Merge pull request #4956 from freqtrade/dependabot/pip/develop/pytest-cov-2.12.0
Bump pytest-cov from 2.11.1 to 2.12.0
2021-05-17 09:15:43 +02:00
Matthias
c17d93f387 Merge pull request #4957 from freqtrade/dependabot/pip/develop/flake8-tidy-imports-4.3.0
Bump flake8-tidy-imports from 4.2.1 to 4.3.0
2021-05-17 09:14:43 +02:00
Matthias
5f2fe24d7d Merge pull request #4955 from freqtrade/dependabot/pip/develop/ccxt-1.50.6
Bump ccxt from 1.49.73 to 1.50.6
2021-05-17 07:46:33 +02:00
dependabot[bot]
c0b61282fb Bump jinja2 from 2.11.3 to 3.0.0
Bumps [jinja2](https://github.com/pallets/jinja) from 2.11.3 to 3.0.0.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/2.11.3...3.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-17 05:21:52 +00:00
dependabot[bot]
8143e63853 Bump aiofiles from 0.6.0 to 0.7.0
Bumps [aiofiles](https://github.com/Tinche/aiofiles) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/Tinche/aiofiles/releases)
- [Commits](https://github.com/Tinche/aiofiles/compare/v0.6.0...v0.7.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-17 05:21:36 +00:00
dependabot[bot]
976a026d3b Bump sqlalchemy from 1.4.14 to 1.4.15
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.14 to 1.4.15.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-17 05:21:31 +00:00
dependabot[bot]
439ef197bc Bump flake8-tidy-imports from 4.2.1 to 4.3.0
Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 4.2.1 to 4.3.0.
- [Release notes](https://github.com/adamchainz/flake8-tidy-imports/releases)
- [Changelog](https://github.com/adamchainz/flake8-tidy-imports/blob/main/HISTORY.rst)
- [Commits](https://github.com/adamchainz/flake8-tidy-imports/compare/4.2.1...4.3.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-17 05:21:18 +00:00
dependabot[bot]
78c77cca73 Bump pytest-cov from 2.11.1 to 2.12.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.11.1 to 2.12.0.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.11.1...v2.12.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-17 05:21:11 +00:00
dependabot[bot]
b0f854af95 Bump ccxt from 1.49.73 to 1.50.6
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.49.73 to 1.50.6.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst)
- [Commits](https://github.com/ccxt/ccxt/compare/1.49.73...1.50.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-17 05:21:02 +00:00
Matthias
37b71b8cfd Fix PerformanceFilter failing in test-pairlist mode 2021-05-16 19:55:13 +02:00
Matthias
0d50e99563 Fix Agefilter checking for > instead of >= 2021-05-16 19:35:30 +02:00
Matthias
c9ac67e985 Fix some typos 2021-05-16 14:50:25 +02:00
Matthias
8f8d5dbff5 Add tests for sl_order_with_result 2021-05-16 14:41:50 +02:00
Matthias
6f38976470 Introduce cancel_stoploss_with_result 2021-05-16 14:15:24 +02:00
Matthias
380754b8ab Fix typos in docstrings 2021-05-16 13:20:05 +02:00
Matthias
0b1dd0d203 Use correct order_id for ftx
closes #4511
2021-05-16 09:15:25 +02:00
Matthias
32bdceee12 Merge pull request #4947 from freqtrade/performance_abs
/performance - sort by absolute profit
2021-05-15 20:32:58 +02:00
Matthias
2d7735ba04 Update telegram to sort performance by absolute performance 2021-05-15 19:49:21 +02:00
Matthias
6b2a38ccfb Add absolute Profit to apiserver 2021-05-15 19:39:46 +02:00
Matthias
2ecb42a639 Improve rest-api doc config samples 2021-05-15 15:52:02 +02:00
Matthias
4a11688e33 Merge pull request #4945 from JoeSchr/patch-4
Update installation.md
2021-05-15 15:38:28 +02:00
JoeSchr
8e98778498 Update installation.md
Fix typo
2021-05-15 15:21:21 +02:00
Matthias
4f968b4a6f Merge pull request #4926 from rokups/rk/misc-fixes
Two fixes
2021-05-15 15:11:07 +02:00
Brook Miles
db17b1a851 fix indentation 2021-05-15 20:20:36 +09:00
Brook Miles
88da1f109b fix #4412 download-data does not stop downloading at the specified TIMERANGE end date 2021-05-15 20:15:19 +09:00
Rokas Kupstys
2d5f465f1b Fix protections being loaded multiple times for first strategy when backtesting. 2021-05-15 13:37:03 +03:00
Rokas Kupstys
29fed37df3 Fix exception when few pairs with no data do not result in aborting backtest.
Exception is triggered by backtesting 20210301-20210501 range with BAKE/USDT pair (binance). Pair data starts on 2021-04-30 12:00:00 and after adjusting for startup candles pair dataframe is empty.

Solution: Since there are other pairs with enough data - skip pairs with no data and issue a warning.

Exception:
```
Traceback (most recent call last):
  File "/home/rk/src/freqtrade/freqtrade/main.py", line 37, in main
    return_code = args['func'](args)
  File "/home/rk/src/freqtrade/freqtrade/commands/optimize_commands.py", line 53, in start_backtesting
    backtesting.start()
  File "/home/rk/src/freqtrade/freqtrade/optimize/backtesting.py", line 502, in start
    min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
  File "/home/rk/src/freqtrade/freqtrade/optimize/backtesting.py", line 474, in backtest_one_strategy
    results = self.backtest(
  File "/home/rk/src/freqtrade/freqtrade/optimize/backtesting.py", line 365, in backtest
    data: Dict = self._get_ohlcv_as_lists(processed)
  File "/home/rk/src/freqtrade/freqtrade/optimize/backtesting.py", line 199, in _get_ohlcv_as_lists
    pair_data.loc[:, 'buy'] = 0  # cleanup from previous run
  File "/home/rk/src/freqtrade/venv/lib/python3.9/site-packages/pandas/core/indexing.py", line 692, in __setitem__
    iloc._setitem_with_indexer(indexer, value, self.name)
  File "/home/rk/src/freqtrade/venv/lib/python3.9/site-packages/pandas/core/indexing.py", line 1587, in _setitem_with_indexer
    raise ValueError(
ValueError: cannot set a frame with no defined index and a scalar
```
2021-05-15 13:37:03 +03:00
Matthias
e1447f955c /locks should always respond, even if there's no locks
closes #4942
2021-05-15 10:50:00 +02:00
Brook Miles
2eac23a15f if stoploss price is above the candle high, set it to candle open instead. this can occur if stoploss had previously been reached but the sell was prevented by confirm_trade_exit 2021-05-15 15:38:51 +09:00
Matthias
0ace35bf3d Fix unreferenced error 2021-05-15 08:14:50 +02:00
Matthias
a6c644161d Merge pull request #4930 from freqtrade/hyperopt_memory
Hyperopt memory problems
2021-05-15 07:12:57 +02:00
Matthias
5e73195b30 Use linux lineseperator at all times 2021-05-15 07:01:32 +02:00
Matthias
3aaf06a3e2 Merge pull request #4932 from freqtrade/fix/4920
Change rate back to "open" for custom_sell
2021-05-15 06:40:21 +02:00
Matthias
1f049214aa Merge pull request #4933 from rokups/patch-1
Couple tweaks for docs.
2021-05-14 20:04:16 +02:00
Rokas Kupstys
330fb538a9 Couple tweaks for docs. 2021-05-14 10:43:48 +03:00
Matthias
09b6923e50 Use "choose" link for new issues 2021-05-14 07:22:51 +02:00
Matthias
4bc018a456 Change rate back to "open" for custom_sell
closes #4920
2021-05-14 07:18:10 +02:00
Matthias
09756e3007 Subplots should always be included in responses 2021-05-14 06:36:50 +02:00
Matthias
ff7bbec1bc Merge pull request #4902 from docbobo/arm64-docs
Documentation for running arm64 builds
2021-05-14 05:48:08 +02:00
Matthias
ecee42f561 Read pickle file in mmap mode 2021-05-13 20:13:04 +02:00
Matthias
1055862bc0 Extract data-load + dump from hyperopt
(Reduces memory-usage as the dataframes go out of scope)
2021-05-12 21:15:01 +02:00
Matthias
5e66d37d57 Slightly modify docker instructions for arm64 2021-05-12 20:07:45 +02:00
Matthias
84222c89ee Merge pull request #4922 from rokups/rk/fix-docs
Docs update regarding dataframe access
2021-05-12 19:58:11 +02:00
Matthias
24a1d5a96f Change default hyperopt-name to be shorter 2021-05-12 19:06:13 +02:00
Matthias
5f5597b93f Better test hyperopt writing and reading 2021-05-12 19:06:09 +02:00
Rokas Kupstys
9bb6ba086b Update docs/strategy-advanced.md
Co-authored-by: Matthias <xmatthias@outlook.com>
2021-05-12 17:15:38 +03:00
Rokas Kupstys
ad4c51b3c5 * Added "Dataframe access" section showcasing how to obtain dataframe and use it to get last-available and trade-open candles.
* Fix custom_sell() example to use rsi from last-available instead of trade-open candle, add a pointer to "Dataframe access" section for more info.
* Simplify "Custom stoploss using an indicator from dataframe example" greatly, add a pointer to "Dataframe access" section for more info.
2021-05-12 09:30:35 +03:00
Matthias
3cbe40875d read hyperopt results from pickle or json 2021-05-12 06:06:30 +02:00
Matthias
06bf1aa274 Store epochs as json per line 2021-05-12 05:58:25 +02:00
Matthias
7398ea88e0 Change optimize_reports to convert dates to string earlier 2021-05-11 20:37:49 +02:00
Matthias
6d232db1d8 Merge pull request #4918 from kamontat/patch-1
remove duplicate python3-pip
2021-05-11 20:16:26 +02:00
Kamontat Chantrachirathumrong
e53bbec285 remove duplicate python3-pip 2021-05-12 00:13:13 +07:00
Matthias
bcab44560a Fix doc typo 2021-05-11 06:25:23 +02:00
Matthias
c4a2de0fd5 Merge pull request #4915 from froggleston/develop
Small update custom_sell() doc
2021-05-11 06:16:49 +02:00
Robert Davey
ac95d577a5 Merge pull request #1 from froggleston/frog-custom-sell-doc
Update strategy-advanced.md
2021-05-10 19:48:09 +01:00
Robert Davey
425d97719a Update strategy-advanced.md
Update custom_sell() example to comment that the current trade row is at trade open as written. Change "abstain" to something clearer for non-fluent English speakers.
2021-05-10 19:42:37 +01:00
Matthias
b81f24d9c6 Merge pull request #4851 from rokups/rk/backtest-dataprovider
Data provider support in backtesting
2021-05-10 19:11:05 +02:00
Matthias
91fd367287 Merge pull request #4906 from freqtrade/dependabot/pip/develop/ccxt-1.49.73
Bump ccxt from 1.49.30 to 1.49.73
2021-05-10 15:24:02 +02:00
Matthias
3d6b3f1d6a Add Issue config.yml 2021-05-10 15:08:28 +02:00
dependabot[bot]
8e6a95e11b Bump ccxt from 1.49.30 to 1.49.73
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.49.30 to 1.49.73.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst)
- [Commits](https://github.com/ccxt/ccxt/compare/1.49.30...1.49.73)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 10:50:05 +00:00
Matthias
7e6d469511 Merge pull request #4913 from freqtrade/dependabot/pip/develop/fastapi-0.64.0
Bump fastapi from 0.63.0 to 0.64.0
2021-05-10 08:53:34 +02:00
Matthias
9cef2983e5 Merge pull request #4907 from freqtrade/dependabot/pip/develop/mkdocs-material-7.1.4
Bump mkdocs-material from 7.1.3 to 7.1.4
2021-05-10 08:52:02 +02:00
Matthias
3eb4cda975 Merge pull request #4911 from freqtrade/dependabot/pip/develop/pymdown-extensions-8.2
Bump pymdown-extensions from 8.1.1 to 8.2
2021-05-10 08:51:47 +02:00
Matthias
0fefa696cc Merge pull request #4908 from freqtrade/dependabot/pip/develop/sqlalchemy-1.4.14
Bump sqlalchemy from 1.4.12 to 1.4.14
2021-05-10 08:50:42 +02:00
Matthias
837c05aa62 Merge pull request #4910 from freqtrade/dependabot/pip/develop/flake8-3.9.2
Bump flake8 from 3.9.1 to 3.9.2
2021-05-10 08:49:53 +02:00
Matthias
699b21064b Merge pull request #4912 from freqtrade/dependabot/pip/develop/pytest-mock-3.6.1
Bump pytest-mock from 3.6.0 to 3.6.1
2021-05-10 08:49:25 +02:00
Matthias
158a4ea660 Merge pull request #4909 from freqtrade/dependabot/pip/develop/pytest-6.2.4
Bump pytest from 6.2.3 to 6.2.4
2021-05-10 08:49:13 +02:00
dependabot[bot]
43c7382d24 Bump fastapi from 0.63.0 to 0.64.0
Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.63.0 to 0.64.0.
- [Release notes](https://github.com/tiangolo/fastapi/releases)
- [Commits](https://github.com/tiangolo/fastapi/compare/0.63.0...0.64.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 05:27:36 +00:00
dependabot[bot]
93268ba16d Bump pytest-mock from 3.6.0 to 3.6.1
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.6.0 to 3.6.1.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.6.0...v3.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 05:27:22 +00:00
dependabot[bot]
5ecd86ed56 Bump pymdown-extensions from 8.1.1 to 8.2
Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 8.1.1 to 8.2.
- [Release notes](https://github.com/facelessuser/pymdown-extensions/releases)
- [Commits](https://github.com/facelessuser/pymdown-extensions/compare/8.1.1...8.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 05:27:11 +00:00
dependabot[bot]
6eb47b0aeb Bump flake8 from 3.9.1 to 3.9.2
Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.9.1 to 3.9.2.
- [Release notes](https://gitlab.com/pycqa/flake8/tags)
- [Commits](https://gitlab.com/pycqa/flake8/compare/3.9.1...3.9.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 05:27:03 +00:00
dependabot[bot]
0a82e2b061 Bump pytest from 6.2.3 to 6.2.4
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.3 to 6.2.4.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.3...6.2.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 05:26:52 +00:00
dependabot[bot]
a7cd8fc578 Bump sqlalchemy from 1.4.12 to 1.4.14
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.12 to 1.4.14.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 05:26:36 +00:00
dependabot[bot]
a7bd051f6b Bump mkdocs-material from 7.1.3 to 7.1.4
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.3 to 7.1.4.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.3...7.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 05:26:26 +00:00
Matthias
d495ea3693 Update docs about availability of get_analyzed 2021-05-09 19:53:41 +02:00
Matthias
1c408c0404 Add small tests for backtest mode 2021-05-09 19:47:37 +02:00
Boris Prüßmann
f2add44253 Update docs/docker_quickstart.md
Co-authored-by: Matthias <xmatthias@outlook.com>
2021-05-09 17:27:30 +02:00
Boris Pruessmann
0a0e7ce5f5 Documentation for running arm64 builds 2021-05-09 14:37:56 +02:00
Matthias
00e93dad02 Fix mistake in the docs 2021-05-09 10:04:56 +02:00
Matthias
92186d89a2 Add some changes to strategytemplate 2021-05-09 09:56:36 +02:00
Matthias
2157923aee have edge send multiple messages if necessary
closes #4519
2021-05-08 19:45:34 +02:00
Rokas Kupstys
17b9e898d2 Update docs displaying how to get last available and trade-open candles. 2021-05-08 18:40:49 +03:00
Rokas Kupstys
8d8c782bd0 Slice dataframe in backtesting, preventing access to rows past current time. 2021-05-08 18:40:49 +03:00
Matthias
0ca0ed4117 Merge pull request #4843 from docbobo/arm64-support
Docker support for arm64
2021-05-08 17:35:19 +02:00
Matthias
70189b1992 Move dockerfile and document M1 image existance 2021-05-08 17:24:41 +02:00
Rokas Kupstys
f1eb653545 Fix strategy protections not being loaded in backtesting. 2021-05-08 10:29:47 +03:00
Rokas Kupstys
1b01ad6f85 Make exchange parameter optional and do not use it as parameter in backtesting. 2021-05-08 10:29:47 +03:00
Matthias
4b6cd69c81 Add test for no-exchange dataprovider 2021-05-08 10:29:47 +03:00
Rokas Kupstys
d344194b36 Fix dataprovider in hyperopt. 2021-05-08 10:29:47 +03:00
Rokas Kupstys
9b4f6b41a2 Use correct datetime. 2021-05-08 10:29:47 +03:00
Rokas Kupstys
6fb4d83ab3 Fix dataprovider in hyperopt. 2021-05-08 10:29:47 +03:00
Rokas Kupstys
6af4de8fe8 Remove dataframe parameter from docs. 2021-05-08 10:29:47 +03:00
Rokas Kupstys
cdfa6adbe5 Store pair datafrmes in dataprovider for backtesting. 2021-05-08 10:29:47 +03:00
Rokas Kupstys
dc6e702fec Pass current_time to confirm_trade_entry/confirm_trade_exit. 2021-05-08 10:29:47 +03:00
Rokas Kupstys
d34da3f981 Revert "Add dataframe parameter to custom_stoploss() and custom_sell() methods."
This reverts commit 595b8735f8.

# Conflicts:
#	freqtrade/optimize/backtesting.py
#	freqtrade/strategy/interface.py
2021-05-08 10:29:47 +03:00
Matthias
513be11fd9 Fix hyperopt output
closes #4892
2021-05-07 20:23:11 +02:00
Matthias
4a7d7a5779 Merge pull request #4887 from freqtrade/timerange_noarrow
Don't use Arrow to get min/max backtest dates
2021-05-07 06:51:16 +02:00
Matthias
554f5f14b6 Raise exception if no data is left 2021-05-07 06:41:15 +02:00
Matthias
4f529fe424 Don't use Arrow to get min/max backtest dates 2021-05-06 19:43:14 +02:00
Matthias
32577cc0cd Merge pull request #4836 from bzed/telegram-locks
Telegram rpc: split too long /locks messages
2021-05-05 20:15:13 +02:00
Matthias
d8de871934 Merge pull request #4869 from freqtrade/edge_informative
Have Edge support informative pairs
2021-05-05 20:10:40 +02:00
Matthias
a710b7dc01 Update tests to match new behaviour 2021-05-05 19:58:45 +02:00
Matthias
431cb5313f Support informative pairs in edge positioning 2021-05-05 19:58:45 +02:00
Matthias
6c50157c95 Merge pull request #4877 from freqtrade/dependabot/docker/python-3.9.5-slim-buster
Bump python from 3.9.4-slim-buster to 3.9.5-slim-buster
2021-05-05 08:31:34 +02:00
Matthias
947ad856c0 Update Dockerfile.armhf 2021-05-05 08:12:28 +02:00
dependabot[bot]
f55ce04fa6 Bump python from 3.9.4-slim-buster to 3.9.5-slim-buster
Bumps python from 3.9.4-slim-buster to 3.9.5-slim-buster.

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-05 05:14:42 +00:00
Matthias
da47f4e1a4 Fix Kraken balance update error
closes #4873
2021-05-05 06:47:26 +02:00
Matthias
4d9dc2a2ff Merge pull request #4848 from freqtrade/hyperopt_btresults
Hyperopt store backtest-outcome
2021-05-04 06:44:01 +02:00
Matthias
da5e832a5a Merge pull request #4862 from freqtrade/dependabot/pip/develop/python-telegram-bot-13.5
Bump python-telegram-bot from 13.4.1 to 13.5
2021-05-03 13:12:28 +02:00
Matthias
239583d7bc Merge pull request #4859 from freqtrade/dependabot/pip/develop/cachetools-4.2.2
Bump cachetools from 4.2.1 to 4.2.2
2021-05-03 13:12:18 +02:00
Matthias
b852a8b1c6 Merge pull request #4861 from freqtrade/dependabot/pip/develop/ccxt-1.49.30
Bump ccxt from 1.48.76 to 1.49.30
2021-05-03 13:12:04 +02:00
dependabot[bot]
860379bc58 Bump ccxt from 1.48.76 to 1.49.30
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.48.76 to 1.49.30.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst)
- [Commits](https://github.com/ccxt/ccxt/compare/1.48.76...1.49.30)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-03 10:23:52 +00:00
dependabot[bot]
2d89824267 Bump cachetools from 4.2.1 to 4.2.2
Bumps [cachetools](https://github.com/tkem/cachetools) from 4.2.1 to 4.2.2.
- [Release notes](https://github.com/tkem/cachetools/releases)
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v4.2.1...v4.2.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-03 10:21:49 +00:00
dependabot[bot]
82a08bd7de Bump python-telegram-bot from 13.4.1 to 13.5
Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.4.1 to 13.5.
- [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases)
- [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.4.1...v13.5)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-03 10:16:58 +00:00
Matthias
42c286a2da Merge pull request #4863 from freqtrade/dependabot/pip/develop/arrow-1.1.0
Bump arrow from 1.0.3 to 1.1.0
2021-05-03 10:13:22 +02:00
Matthias
f138cca797 Be explicit with space assignment in documentation 2021-05-03 08:33:22 +02:00
Matthias
4465915a94 Merge pull request #4856 from freqtrade/dependabot/pip/develop/scikit-learn-0.24.2
Bump scikit-learn from 0.24.1 to 0.24.2
2021-05-03 08:23:06 +02:00
Matthias
f55ce8543a Merge pull request #4860 from freqtrade/dependabot/pip/develop/technical-1.3.0
Bump technical from 1.2.2 to 1.3.0
2021-05-03 08:22:13 +02:00
Matthias
d7e4d5ff3f Merge pull request #4858 from freqtrade/dependabot/pip/develop/pyjwt-2.1.0
Bump pyjwt from 2.0.1 to 2.1.0
2021-05-03 08:20:51 +02:00
Matthias
39cffeb2df Merge pull request #4857 from freqtrade/dependabot/pip/develop/sqlalchemy-1.4.12
Bump sqlalchemy from 1.4.11 to 1.4.12
2021-05-03 08:20:13 +02:00
dependabot[bot]
a63d9e9515 Bump arrow from 1.0.3 to 1.1.0
Bumps [arrow](https://github.com/arrow-py/arrow) from 1.0.3 to 1.1.0.
- [Release notes](https://github.com/arrow-py/arrow/releases)
- [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/arrow-py/arrow/compare/1.0.3...1.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-03 05:27:17 +00:00
dependabot[bot]
cea207026a Bump technical from 1.2.2 to 1.3.0
Bumps [technical](https://github.com/freqtrade/technical) from 1.2.2 to 1.3.0.
- [Release notes](https://github.com/freqtrade/technical/releases)
- [Commits](https://github.com/freqtrade/technical/compare/1.2.2...1.3.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-03 05:26:39 +00:00
dependabot[bot]
37227170b3 Bump pyjwt from 2.0.1 to 2.1.0
Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.0.1 to 2.1.0.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.0.1...2.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-03 05:26:30 +00:00
dependabot[bot]
8ed15fb7cc Bump sqlalchemy from 1.4.11 to 1.4.12
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.11 to 1.4.12.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-03 05:26:18 +00:00
dependabot[bot]
8364343cd6 Bump scikit-learn from 0.24.1 to 0.24.2
Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 0.24.1 to 0.24.2.
- [Release notes](https://github.com/scikit-learn/scikit-learn/releases)
- [Commits](https://github.com/scikit-learn/scikit-learn/compare/0.24.1...0.24.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-03 05:26:08 +00:00
Matthias
da574e4e69 Small style fixes 2021-05-03 06:30:41 +02:00
Matthias
6d7096dc66 Use both old and new fixtures for test 2021-05-02 20:42:01 +02:00
Matthias
fc110ea418 Support csv export for new and old versions 2021-05-02 20:41:45 +02:00
Matthias
303895b33e Add support for filters to new hyperopt-results 2021-05-02 20:07:22 +02:00
Matthias
ef9dd0676c Rename hyperoptresult fixture to avoid naming collision 2021-05-02 20:06:47 +02:00
Matthias
99e1ef9b4a Fix docs typo for CategoryParameter
closes #4852
2021-05-02 19:21:26 +02:00
Matthias
6f8225c49e Merge pull request #4850 from youpas/patch-2
Fixed anchor link for PriceFilter
2021-05-02 17:08:59 +02:00
Matthias
60e908eee5 Merge pull request #4849 from youpas/patch-1
Fixed syntax error in the example
2021-05-02 15:10:50 +02:00
youpas
b71d850596 Fixed anchor link for PriceFilter 2021-05-02 11:47:02 +02:00
youpas
4fc37f15d1 Fixed syntax error in the example
Removed extra comma in the "Full example of Pairlist Handlers" section.
2021-05-02 11:41:26 +02:00
Matthias
287b43e999 Output strategy results including non-optimized parameters 2021-05-02 11:30:53 +02:00
Matthias
d069ad43d8 Small reformatting in hyperopt 2021-05-02 11:01:26 +02:00
Matthias
8ee0b0d8e8 Store not optimized parameters (if applicable) 2021-05-02 10:46:04 +02:00
Matthias
9049d6b779 Reformat hyper to cache parameters 2021-05-02 10:45:21 +02:00
Matthias
6b6270db13 Add hint about "sell_profit_only" to docs 2021-05-02 10:37:54 +02:00
Matthias
46f0f66039 Keep dimensions stored in hyperopt class
There is no point in regenerating them and it will cause some
overhead as all space classes will be recreated for every epoch.
2021-05-02 09:48:37 +02:00
Matthias
ced5cc7ce2 Don't recalculate min/max date - they won't change between epochs 2021-05-02 09:46:27 +02:00
Matthias
ecdfb6e5ed Fix output of % for new format 2021-05-02 09:46:27 +02:00
Matthias
881cba336a Show backtesting result in hyperopt-show 2021-05-02 09:46:27 +02:00
Matthias
420e75af65 Extract show_backtest_result for one strategy 2021-05-02 09:46:27 +02:00
Matthias
97478abb9d Move format explanation string to HyperoptTools 2021-05-02 09:46:27 +02:00
Matthias
4c00d4496d Update tests to reflect new backtest returns 2021-05-02 09:46:27 +02:00
Matthias
f2e182002d Simplify calling backtesting by returning the proper result 2021-05-02 09:46:27 +02:00
Matthias
e2e1d34828 Extract stake_currency param from hyperopt-explanationstring 2021-05-02 09:46:27 +02:00
Matthias
852f125347 Fix tests 2021-05-02 09:46:27 +02:00
Matthias
6aaaad29d7 Use backtesting output for hyperopt results 2021-05-02 09:46:27 +02:00
Matthias
545cba7fd8 Refactor optimize_report
we should not calculate non-daily statistics in the daily stats method
2021-05-02 09:46:27 +02:00
Matthias
9994fce577 Extract generation of report for one strategy to it's own method 2021-05-02 09:46:27 +02:00
Matthias
b125c975c7 Rename strategy_comparison method 2021-05-02 09:46:27 +02:00
Matthias
c45204a2c4 Fix failing mocks 2021-05-02 08:59:21 +02:00
Matthias
ac2e1eb3d7 Don't import joblib for regular strategies 2021-05-02 08:44:16 +02:00
Matthias
0e359dcb7a Merge pull request #4788 from saeedrss/develop
fetch live data from hitbtc exchange #4778
2021-05-01 19:13:00 +02:00
Matthias
bdd0184f0b Small stylistic fixes 2021-05-01 17:44:43 +02:00
Matthias
3c9042c825 Merge pull request #4844 from freqtrade/improve_hyper
IntParameter should not calculate indicators if it's not being optimized
2021-05-01 17:36:29 +02:00
Matthias
e01bc7717b Merge pull request #4832 from JoeSchr/patch-3
Add to custom_sell() documentation
2021-05-01 17:34:10 +02:00
Matthias
0b280a59bc Support per exchange params for OHLCV endpoint 2021-05-01 17:29:53 +02:00
Matthias
ca0749dfdd Update strategy-advanced.md 2021-05-01 16:58:14 +02:00
Matthias
555262b6e1 Only calculate additional indicators if the space is selected 2021-05-01 16:40:29 +02:00
Matthias
e381df9098 extract has_space to Hyperopt-Tools 2021-05-01 16:36:35 +02:00
Matthias
e050ea8dfa Don't load parameters for other space 2021-05-01 16:21:59 +02:00
Boris Pruessmann
42a52ff669 Docker support for arm64 2021-05-01 14:13:21 +02:00
Matthias
401f31e86b Merge pull request #4841 from JoeSchr/fix/istrategy-return-value
fix IStrategy: abstract methods still need to pass through return value
2021-04-30 20:13:11 +02:00
Matthias
856b65206b Reduce log-frequency of AgeFilter
closes #4840
2021-04-30 19:42:41 +02:00
Joe Schr
f3388ed9aa fix IStrategy: abstract methods still need to pass through return value
otherwise doing something like:
```py
dataframe = super().populate_indicators(dataframe, ...)
```

won't work, because `dataframe` becomes `None`.

This is needed if one of those methods uses dataframe.copy() instead of
just working on reference. e.g. using `merge_informative` in
`populate_indicator` in a nested class hierarchy
2021-04-30 14:39:01 +02:00
Matthias
6763bd447e Fix link to poweredby image 2021-04-30 07:50:33 +02:00
Bernd Zeimetz
3be7bc509c Telegram: send locks as chunks of 25.
Producing easily readable messages, hopefully always below the message lenght limit
2021-04-29 22:21:04 +02:00
Bernd Zeimetz
4d1613a432 Add chunks function.
Implementing a generator to split Lists into chunks.
2021-04-29 22:21:04 +02:00
JoeSchr
f2bd70dfc2 Add sentence about how it differs from custom_stoploss() 2021-04-29 13:07:22 +02:00
Matthias
27a9b2cc9f Merge pull request #4831 from JoeSchr/patch-2
Update strategy-advanced.md
2021-04-29 12:51:52 +02:00
JoeSchr
cf839e36f3 Add to custom_sell() documentation
- Flesh out infos about return type 
- give quick example at beginning to get reader in right mindset what this does and why it's useful
2021-04-29 12:49:51 +02:00
Matthias
bb115154eb Merge pull request #4829 from nmcc1212/patch-1
Docker Quick start grammatical error
2021-04-29 12:30:36 +02:00
JoeSchr
83708ae045 Update strategy-advanced.md
Remove untrue comment probably left-over from more intricate example
2021-04-29 12:16:02 +02:00
Nial McCallister
7cf8c5d659 Docker Quick start grammatical error
please install docker-compose should be installed
 does not make grammatical sense
2021-04-29 10:46:00 +01:00
Matthias
5bc908870f Fix documentation comment missalignment 2021-04-29 09:07:47 +02:00
Matthias
a3d2e68312 Merge pull request #4750 from rokups/rk/custom_sell
Add IStrategy.custom_sell method which allows per-trade sell signal evaluation
2021-04-29 06:50:56 +02:00
Matthias
aab020c9a2 Merge pull request #4818 from freqtrade/cleanup_models
Move static Trade functions to right class
2021-04-28 21:18:55 +02:00
Matthias
3285f6caa3 Improve wording in Note box 2021-04-28 20:42:15 +02:00
Matthias
7c8a367442 Update docs to not promote stoploss / take-profit 2021-04-28 20:36:06 +02:00
Matthias
6eb947ae09 Move static Trade functions to right class 2021-04-28 06:37:40 +02:00
Matthias
cc916ab2e9 Add test for custom_sell 2021-04-26 20:26:14 +02:00
Matthias
55faa6a84a safe_wrapper should use kwargs to call methods 2021-04-26 20:18:03 +02:00
Matthias
2061162d79 Convert trade-opendate to python datetime 2021-04-26 20:01:13 +02:00
Matthias
dbf33271b5 Small doc changes 2021-04-26 19:52:26 +02:00
Rokas Kupstys
31a2285eac Fix mypy complaints. 2021-04-26 10:42:24 +03:00
Rokas Kupstys
9c21c75cf5 Fix inaccuracy in docs. 2021-04-25 13:18:35 +03:00
Rokas Kupstys
98f6fce2ec Use correct sell reason in case of custom sell reason. 2021-04-25 09:48:40 +03:00
Rokas Kupstys
e58fe7a8cb Update custom_sell documentation. 2021-04-25 09:48:40 +03:00
Rokas Kupstys
004550529e Document dataframe parameter in custom_stoploss(). 2021-04-25 09:48:40 +03:00
Rokas Kupstys
595b8735f8 Add dataframe parameter to custom_stoploss() and custom_sell() methods. 2021-04-25 09:48:40 +03:00
Rokas Kupstys
961b38636f Remove explicit sell_flag parameter from SellCheckTuple. 2021-04-25 09:48:40 +03:00
Rokas Kupstys
bfad4e82ad Make execute_sell() use SellCheckTuple for sell reason. 2021-04-25 09:48:40 +03:00
Rokas Kupstys
a90e795695 Warn and trim custom sell reason if it is too long. 2021-04-25 09:48:40 +03:00
Rokas Kupstys
1aad128d85 Support returning a string from custom_sell() and have it recorded as custom sell reason. 2021-04-25 09:48:40 +03:00
Rokas Kupstys
a77337e424 Document IStrategy.custom_sell. 2021-04-25 09:48:40 +03:00
Rokas Kupstys
1292e08fe4 Use strategy_safe_wrapper() when calling custom_sell(). 2021-04-25 09:48:40 +03:00
Rokas Kupstys
0fd68aee51 Add IStrategy.custom_sell method which allows per-trade sell signal evaluation. 2021-04-25 09:48:40 +03:00
saeedrss
37d8e3c758 Merge branch 'develop' of https://github.com/freqtrade/freqtrade into develop 2021-04-23 22:28:33 +04:30
saeedrss
b69a9134f5 fixing support for HitBTC #4778
hitbtc by default send candle from beginning (not most recently)
this change fixed
2021-04-23 21:27:13 +04:30
Gonzalo Matheu
7a98de10ea telegram: Formatting typings 2021-04-15 21:51:16 -03:00
Gonzalo Matheu
e3c5a4b3fc telegram: Formatting imports 2021-04-15 21:51:16 -03:00
Gonzalo Matheu
5fae4ea2fd telegram: Formatting code 2021-04-15 21:51:16 -03:00
Gonzalo Matheu
50bdae8eb2 telegram: Adding forcebuy inline keyboard 2021-04-15 21:51:16 -03:00
Gonzalo Matheu
e2f28991e6 telegram: Wrapping long line 2021-04-15 21:51:16 -03:00
Gonzalo Matheu
271e4500d9 telegram: Adding dynamic keyboard to /forcebuy response 2021-04-15 21:51:16 -03:00
95 changed files with 3014 additions and 1457 deletions

View File

@@ -1,20 +1,21 @@
FROM freqtradeorg/freqtrade:develop
USER root
# Install dependencies
COPY requirements-dev.txt /freqtrade/
RUN apt-get update \
&& apt-get -y install git mercurial sudo vim \
&& apt-get -y install git mercurial sudo vim build-essential \
&& apt-get clean \
&& pip install autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir \
&& useradd -u 1000 -U -m ftuser \
&& mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \
&& echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \
&& echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \
&& mv /root/.local /home/ftuser/.local/ \
&& chown ftuser:ftuser -R /home/ftuser/.local/ \
&& chown ftuser: -R /home/ftuser/
USER ftuser
RUN pip install --user autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir
# Empty the ENTRYPOINT to allow all commands
ENTRYPOINT []

6
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
---
blank_issues_enabled: false
contact_links:
- name: Discord Server
url: https://discord.gg/MA9v74M
about: Ask a question or get community support from our Discord server

View File

@@ -301,7 +301,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Cleanup previous runs on this branch
uses: rokroskar/workflow-run-cleanup-action@v0.3.2
uses: rokroskar/workflow-run-cleanup-action@v0.3.3
if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -1,4 +1,4 @@
FROM python:3.9.4-slim-buster as base
FROM python:3.9.5-slim-buster as base
# Setup env
ENV LANG C.UTF-8

View File

@@ -1,4 +1,4 @@
# ![freqtrade](docs/assets/freqtrade_poweredby.svg)
# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg)
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
@@ -154,7 +154,7 @@ You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/
If you discover a bug in the bot, please
[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
first. If it hasn't been reported, please
[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and
[create a new issue](https://github.com/freqtrade/freqtrade/issues/new/choose) and
ensure you follow the template guide so that our team can assist you as
quickly as possible.
@@ -163,7 +163,7 @@ quickly as possible.
Have you a great idea to improve the bot you want to share? Please,
first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement).
If it hasn't been requested, please
[create a new request](https://github.com/freqtrade/freqtrade/issues/new)
[create a new request](https://github.com/freqtrade/freqtrade/issues/new/choose)
and ensure you follow the template guide so that it does not get lost
in the bug reports.

Binary file not shown.

Binary file not shown.

View File

@@ -8,10 +8,13 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
tar zxvf ta-lib-0.4.0-src.tar.gz
cd ta-lib \
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
&& ./configure --prefix=${INSTALL_LOC}/ \
&& make \
&& make -j$(nproc) \
&& which sudo && sudo make install || make install \
&& cd ..
else
echo "TA-lib already installed, skipping installation"
fi
# && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \

View File

@@ -1,16 +1,15 @@
# Downloads don't work automatically, since the URL is regenerated via javascript.
# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib
# Invoke-WebRequest -Uri "https://download.lfd.uci.edu/pythonlibs/xxxxxxx/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -OutFile "TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl"
python -m pip install --upgrade pip
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
if ($pyv -eq '3.7') {
pip install build_helpers\TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl
pip install build_helpers\TA_Lib-0.4.20-cp37-cp37m-win_amd64.whl
}
if ($pyv -eq '3.8') {
pip install build_helpers\TA_Lib-0.4.19-cp38-cp38-win_amd64.whl
pip install build_helpers\TA_Lib-0.4.20-cp38-cp38-win_amd64.whl
}
pip install -r requirements-dev.txt

View File

@@ -23,7 +23,8 @@
"stoploss": -0.10,
"unfilledtimeout": {
"buy": 10,
"sell": 30
"sell": 30,
"unit": "minutes"
},
"bid_strategy": {
"price_side": "bid",

58
docker/Dockerfile.aarch64 Normal file
View File

@@ -0,0 +1,58 @@
FROM --platform=linux/arm64/v8 python:3.9.4-slim-buster as base
# Setup env
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONFAULTHANDLER 1
ENV PATH=/home/ftuser/.local/bin:$PATH
ENV FT_APP_ENV="docker"
# Prepare environment
RUN mkdir /freqtrade \
&& apt-get update \
&& apt-get -y install libatlas3-base curl sqlite3 libhdf5-serial-dev sudo \
&& apt-get clean \
&& useradd -u 1000 -G sudo -U -m ftuser \
&& chown ftuser:ftuser /freqtrade \
# Allow sudoers
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers
WORKDIR /freqtrade
# Install dependencies
FROM base as python-deps
RUN apt-get update \
&& apt-get -y install curl build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
&& apt-get clean \
&& pip install --upgrade pip
# Install TA-lib
COPY build_helpers/* /tmp/
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
ENV LD_LIBRARY_PATH /usr/local/lib
# Install dependencies
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
USER ftuser
RUN pip install --user --no-cache-dir numpy \
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt
# Copy dependencies to runtime-image
FROM base as runtime-image
COPY --from=python-deps /usr/local/lib /usr/local/lib
ENV LD_LIBRARY_PATH /usr/local/lib
COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local
USER ftuser
# Install and execute
COPY --chown=ftuser:ftuser . /freqtrade/
RUN pip install -e . --user --no-cache-dir \
&& mkdir /freqtrade/user_data/ \
&& freqtrade install-ui
ENTRYPOINT ["freqtrade"]
# Default to trade mode
CMD [ "trade" ]

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -237,29 +237,29 @@ The most important in the backtesting is to understand the result.
A backtesting result will look like that:
```
========================================================= BACKTESTING REPORT ========================================================
| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses |
|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|--------:|
| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 | 0 | 21 |
| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 | 0 | 8 |
| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 | 0 | 14 |
| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 | 0 | 7 |
| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 | 0 | 10 |
| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 | 0 | 20 |
| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 | 0 | 15 |
| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 | 0 | 17 |
| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 | 0 | 18 |
| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 | 0 | 9 |
| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 | 0 | 21 |
| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 | 0 | 7 |
| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 | 0 | 13 |
| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 | 0 | 5 |
| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 | 0 | 9 |
| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 | 0 | 11 |
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 | 0 | 23 |
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 | 0 | 15 |
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 |
========================================================= SELL REASON STATS =========================================================
========================================================= BACKTESTING REPORT ==========================================================
| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% |
|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:|
| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 |
| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 |
| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 |
| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 |
| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 |
| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 |
| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 |
| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 |
| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 |
| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 |
| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 |
| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 |
| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 |
| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 |
| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 |
| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 |
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
========================================================= SELL REASON STATS ==========================================================
| Sell Reason | Sells | Wins | Draws | Losses |
|:-------------------|--------:|------:|-------:|--------:|
| trailing_stop_loss | 205 | 150 | 0 | 55 |
@@ -267,11 +267,11 @@ A backtesting result will look like that:
| sell_signal | 56 | 36 | 0 | 20 |
| force_sell | 2 | 0 | 0 | 2 |
====================================================== LEFT OPEN TRADES REPORT ======================================================
| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses |
|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|--------:|
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 | 0 | 0 |
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 | 0 | 0 |
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 | 0 |
| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
=============== SUMMARY METRICS ===============
| Metric | Value |
|-----------------------+---------------------|
@@ -297,6 +297,8 @@ A backtesting result will look like that:
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 |
| Zero Duration Trades | 4.6% (20) |
| Rejected Buy signals | 3089 |
| | |
| Min balance | 0.00945123 BTC |
| Max balance | 0.01846651 BTC |
@@ -318,7 +320,7 @@ The last line will give you the overall performance of your strategy,
here:
```
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 |
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
```
The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has
@@ -384,6 +386,8 @@ It contains some useful key metrics about performance of your strategy on backte
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 |
| Zero Duration Trades | 4.6% (20) |
| Rejected Buy signals | 3089 |
| | |
| Min balance | 0.00945123 BTC |
| Max balance | 0.01846651 BTC |
@@ -413,6 +417,8 @@ It contains some useful key metrics about performance of your strategy on backte
- `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
- `Zero Duration Trades`: A number of trades that completed within same candle as they opened and had `trailing_stop_loss` sell reason. A significant amount of such trades may indicate that strategy is exploiting trailing stoploss behavior in backtesting and produces unrealistic results.
- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached.
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
- `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost.
@@ -472,11 +478,11 @@ There will be an additional table comparing win/losses of the different strategi
Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
```
=========================================================== STRATEGY SUMMARY ===========================================================
| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses |
|:------------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|
| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 |
| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 |
=========================================================== STRATEGY SUMMARY =========================================================================
| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % |
|:------------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:|
| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 |
| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 |
```
## Next step

View File

@@ -68,8 +68,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled).
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean

View File

@@ -10,7 +10,7 @@ Start by downloading and installing Docker CE for your platform:
* [Windows](https://docs.docker.com/docker-for-windows/install/)
* [Linux](https://docs.docker.com/install/)
To simplify running freqtrade, please install [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start).
To simplify running freqtrade, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start).
## Freqtrade with docker-compose
@@ -48,6 +48,8 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse
# Download the docker-compose file from the repository
curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml
# Edit the compose file to use an image named `*_pi` (stable_pi or develop_pi)
# Pull the freqtrade image
docker-compose pull
@@ -65,6 +67,40 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse
# image: freqtradeorg/freqtrade:develop_pi
```
=== "ARM 64 Systenms (Mac M1, Raspberry Pi 4, Jetson Nano)"
In case of a Mac M1, make sure that your docker installation is running in native mode
Arm64 images are not yet provided via Docker Hub and need to be build locally first.
Depending on the device, this may take a few minutes (Apple M1) or multiple hours (Raspberry Pi)
``` bash
# Clone Freqtrade repository
git clone https://github.com/freqtrade/freqtrade.git
cd freqtrade
# Optionally switch to the stable version
git checkout stable
# Modify your docker-compose file to enable building and change the image name
# (see the Note Box below for necessary changes)
# Build image
docker-compose build
# Create user directory structure
docker-compose run --rm freqtrade create-userdir --userdir user_data
# Create configuration - Requires answering interactive questions
docker-compose run --rm freqtrade new-config --config user_data/config.json
```
!!! Note "Change your docker Image"
You have to change the docker image in the docker-compose file for your arm64 build to work properly.
``` yml
image: freqtradeorg/freqtrade:custom_arm64
build:
context: .
dockerfile: "./docker/Dockerfile.aarch64"
```
The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image.
The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections.

View File

@@ -249,15 +249,16 @@ We continue to define hyperoptable parameters:
```python
class MyAwesomeStrategy(IStrategy):
buy_adx = IntParameter(20, 40, default=30, space="buy")
buy_adx = DecimalParameter(20, 40, decimals=1, default=30.1, space="buy")
buy_rsi = IntParameter(20, 40, default=30, space="buy")
buy_adx_enabled = CategoricalParameter([True, False], space="buy")
buy_rsi_enabled = CategoricalParameter([True, False], space="buy")
buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal'], space="buy")
buy_adx_enabled = CategoricalParameter([True, False], default=True, space="buy")
buy_rsi_enabled = CategoricalParameter([True, False], default=False, space="buy")
buy_trigger = CategoricalParameter(["bb_lower", "macd_cross_signal"], default="bb_lower", space="buy")
```
Above definition says: I have five parameters I want to randomly combine to find the best combination.
Two of them are integer values (`buy_adx` and `buy_rsi`) and I want you test in the range of values 20 to 40.
The above definition says: I have five parameters I want to randomly combine to find the best combination.
`buy_rsi` is an integer parameter, which will be tested between 20 and 40. This space has a size of 20.
`buy_adx` is a decimal parameter, which will be evaluated between 20 and 40 with 1 decimal place (so values are 20.1, 20.2, ...). This space has a size of 200.
Then we have three category variables. First two are either `True` or `False`.
We use these to either enable or disable the ADX and RSI guards.
The last one we call `trigger` and use it to decide which buy trigger we want to use.

View File

@@ -112,6 +112,7 @@ The `PriceFilter` allows filtering of pairs by price. Currently the following pr
* `min_price`
* `max_price`
* `max_value`
* `low_price_ratio`
The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs.
@@ -120,6 +121,11 @@ This option is disabled by default, and will only apply if set to > 0.
The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs.
This option is disabled by default, and will only apply if set to > 0.
The `max_value` setting removes pairs where the minimum value change is above a specified value.
This is useful when an exchange has unbalanced limits. For example, if step-size = 1 (so you can only buy 1, or 2, or 3, but not 1.1 Coins) - and the price is pretty high (like 20$) as the coin has risen sharply since the last limit adaption.
As a result of the above, you can only buy for 20$, or 40$ - but not for 25$.
On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can result in high value coins / amounts that are unsellable as the amount is slightly below the limit.
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
This option is disabled by default, and will only apply if set to > 0.
@@ -193,7 +199,7 @@ If the volatility over the last 10 days is not in the range of 0.05-0.50, remove
### Full example of Pairlist Handlers
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value.
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#pricefilter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value.
```json
"exchange": {
@@ -204,7 +210,7 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets,
{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
"sort_key": "quoteVolume"
},
{"method": "AgeFilter", "min_days_listed": 10},
{"method": "PrecisionFilter"},

View File

@@ -60,7 +60,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
sudo apt-get update
# install packages
sudo apt install -y python3-pip python3-venv python3-pandas python3-pip git
sudo apt install -y python3-pip python3-venv python3-pandas git
```
=== "RaspberryPi/Raspbian"
@@ -269,7 +269,7 @@ git clone https://github.com/freqtrade/freqtrade.git
cd freqtrade
```
#### Freqtrade instal: Conda Environment
#### Freqtrade install: Conda Environment
Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory

View File

@@ -1,3 +1,3 @@
mkdocs-material==7.1.3
mkdocs-material==7.1.5
mdx_truly_sane_lists==1.2
pymdown-extensions==8.1.1
pymdown-extensions==8.2

View File

@@ -71,7 +71,10 @@ If you run your bot using docker, you'll need to have the bot listen to incoming
"api_server": {
"enabled": true,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080
"listen_port": 8080,
"username": "Freqtrader",
"password": "SuperSecret1!",
//...
},
```
@@ -106,7 +109,10 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
"api_server": {
"enabled": true,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080
"listen_port": 8080,
"username": "Freqtrader",
"password": "SuperSecret1!",
//...
}
}
```

View File

@@ -19,7 +19,7 @@ The freqtrade docker image does contain sqlite3, so you can edit the database wi
``` bash
docker-compose exec freqtrade /bin/bash
sqlite3 <databasefile>.sqlite
sqlite3 <database-file>.sqlite
```
## Open the DB
@@ -99,3 +99,32 @@ DELETE FROM trades WHERE id = 31;
!!! Warning
This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause.
## Use a different database system
!!! Warning
By using one of the below database systems, you acknowledge that you know how to manage such a system. Freqtrade will not provide any support with setup or maintenance (or backups) of the below database systems.
### PostgreSQL
Freqtrade supports PostgreSQL by using SQLAlchemy, which supports multiple different database systems.
Installation:
`pip install psycopg2`
Usage:
`... --db-url postgresql+psycopg2://<username>:<password>@localhost:5432/<database>`
Freqtrade will automatically create the tables necessary upon startup.
If you're running different instances of Freqtrade, you must either setup one database per Instance or use different users / schemas for your connections.
### MariaDB / MySQL
Freqtrade supports MariaDB by using SQLAlchemy, which supports multiple different database systems.
Installation:
`pip install pymysql`
Usage:
`... --db-url mysql+pymysql://<username>:<password>@localhost:3306/<database>`

View File

@@ -40,34 +40,79 @@ class AwesomeStrategy(IStrategy):
!!! Note
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
***
## Dataframe access
### Storing custom information using DatetimeIndex from `dataframe`
You may access dataframe in various strategy functions by querying it from dataprovider.
Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps.
``` python
from freqtrade.exchange import timeframe_to_prev_date
```python
import talib.abstract as ta
class AwesomeStrategy(IStrategy):
# Create custom dictionary
custom_info = {}
def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str,
current_time: 'datetime', **kwargs) -> bool:
# Obtain pair dataframe.
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# using "ATR" here as example
dataframe['atr'] = ta.ATR(dataframe)
if self.dp.runmode.value in ('backtest', 'hyperopt'):
# add indicator mapped to correct DatetimeIndex to custom_info
self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date')
return dataframe
# Obtain last available candle. Do not use current_time to look up latest candle, because
# current_time points to curret incomplete candle whose data is not available.
last_candle = dataframe.iloc[-1].squeeze()
# <...>
# In dry/live runs trade open date will not match candle open date therefore it must be
# rounded.
trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
# Look up trade candle.
trade_candle = dataframe.loc[dataframe['date'] == trade_date]
# trade_candle may be empty for trades that just opened as it is still incomplete.
if not trade_candle.empty:
trade_candle = trade_candle.squeeze()
# <...>
```
!!! Warning
The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
!!! Warning "Using .iloc[-1]"
You can use `.iloc[-1]` here because `get_analyzed_dataframe()` only returns candles that backtesting is allowed to see.
This will not work in `populate_*` methods, so make sure to not use `.iloc[]` in that area.
Also, this will only work starting with version 2021.5.
***
## Custom sell signal
It is possible to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision.
For example you could implement a 1:2 risk-reward ROI with `custom_sell()`.
Using custom_sell() signals in place of stoplosses though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
!!! Note
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
See `custom_stoploss` examples below on how to access the saved dataframe columns
An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day:
``` python
class AwesomeStrategy(IStrategy):
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
# Above 20% profit, sell when rsi < 80
if current_profit > 0.2:
if last_candle['rsi'] < 80:
return 'rsi_below_80'
# Between 2% and 10%, sell if EMA-long above EMA-short
if 0.02 < current_profit < 0.1:
if last_candle['emalong'] > last_candle['emashort']:
return 'ema_long_below_80'
# Sell any positions at a loss if they are held for more than one day.
if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1:
return 'unclog'
```
See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
## Custom stoploss
@@ -110,7 +155,7 @@ class AwesomeStrategy(IStrategy):
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the currentrate
:return float: New stoploss value, relative to the current rate
"""
return -0.04
```
@@ -222,7 +267,6 @@ Instead of continuously trailing behind the current price, this example sets fix
* Once profit is > 25% - set stoploss to 15% above open price.
* Once profit is > 40% - set stoploss to 25% above open price.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
@@ -248,63 +292,46 @@ class AwesomeStrategy(IStrategy):
# return maximum stoploss value, keeping current stoploss price unchanged
return 1
```
#### Custom stoploss using an indicator from dataframe example
Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR"
See: "Storing custom information using DatetimeIndex from `dataframe`" example above) on how to store the indicator into `custom_info`
!!! Warning
only use .iat[-1] in live mode, not in backtesting/hyperopt
otherwise you will look into the future
see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info.
Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss.
``` python
from freqtrade.persistence import Trade
from freqtrade.state import RunMode
class AwesomeStrategy(IStrategy):
# ... populate_* methods
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# <...>
dataframe['sar'] = ta.SAR(dataframe)
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
result = 1
if self.custom_info and pair in self.custom_info and trade:
# using current_time directly (like below) will only work in backtesting.
# so check "runmode" to make sure that it's only used in backtesting/hyperopt
if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'):
relative_sl = self.custom_info[pair].loc[current_time]['atr']
# in live / dry-run, it'll be really the current time
else:
# but we can just use the last entry from an already analyzed dataframe instead
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
timeframe=self.timeframe)
# WARNING
# only use .iat[-1] in live mode, not in backtesting/hyperopt
# otherwise you will look into the future
# see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies
relative_sl = dataframe['atr'].iat[-1]
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
if (relative_sl is not None):
# new stoploss relative to current_rate
new_stoploss = (current_rate-relative_sl)/current_rate
# turn into relative negative offset required by `custom_stoploss` return implementation
result = new_stoploss - 1
# Use parabolic sar as absolute stoploss price
stoploss_price = last_candle['sar']
return result
# Convert absolute price to percentage relative to current_rate
if stoploss_price < current_rate:
return (stoploss_price / current_rate) - 1
# return maximum stoploss value, keeping current stoploss price unchanged
return 1
```
See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
---
## Custom order timeout rules
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if a order did time out or not.
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not.
!!! Note
Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
@@ -530,7 +557,7 @@ Both attributes and methods may be overridden, altering behavior of the original
## Embedding Strategies
Freqtrade provides you with with an easy way to embed the strategy into your configuration file.
Freqtrade provides you with an easy way to embed the strategy into your configuration file.
This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field,
in your chosen config file.

View File

@@ -159,7 +159,7 @@ Edit the method `populate_buy_trend()` in your strategy file to update your buy
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
This will method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action".
This method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action".
Sample from `user_data/strategies/sample_strategy.py`:
@@ -193,7 +193,7 @@ Please note that the sell-signal is only used if `use_sell_signal` is set to tru
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
This will method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action".
This method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action".
Sample from `user_data/strategies/sample_strategy.py`:
@@ -422,10 +422,6 @@ if self.dp:
Returns an empty dataframe if the requested pair was not cached.
This should not happen when using whitelisted pairs.
!!! Warning "Warning about backtesting"
This method will return an empty dataframe during backtesting.
### *orderbook(pair, maximum)*
``` python
@@ -633,7 +629,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
# once the profit has risin above 10%, keep the stoploss at 7% above the open price
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10:
return stoploss_from_open(0.07, current_profit)

View File

@@ -250,10 +250,14 @@ Return a summary of your profit/loss and performance.
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
### /forcebuy <pair>
### /forcebuy <pair> [rate]
> **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
Omitting the pair will open a query asking for the pair to buy (based on the current whitelist).
![Telegram force-buy screenshot](assets/telegram_forcebuy.png)
Note that for this to work, `forcebuy_enable` needs to be set to true.
[More details](configuration.md#understand-forcebuy_enable)
@@ -261,12 +265,12 @@ Note that for this to work, `forcebuy_enable` needs to be set to true.
### /performance
Return the performance of each crypto-currency the bot has sold.
> Performance:
> 1. `RCN/BTC 57.77%`
> 2. `PAY/BTC 56.91%`
> 3. `VIB/BTC 47.07%`
> 4. `SALT/BTC 30.24%`
> 5. `STORJ/BTC 27.24%`
> Performance:
> 1. `RCN/BTC 0.003 BTC (57.77%) (1)`
> 2. `PAY/BTC 0.0012 BTC (56.91%) (1)`
> 3. `VIB/BTC 0.0011 BTC (47.07%) (1)`
> 4. `SALT/BTC 0.0010 BTC (30.24%) (1)`
> 5. `STORJ/BTC 0.0009 BTC (27.24%) (1)`
> ...
### /balance

View File

@@ -1,3 +1,5 @@
# Windows installation
We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure).
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
@@ -21,7 +23,7 @@ git clone https://github.com/freqtrade/freqtrade.git
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib0.4.19cp38cp38win_amd64.whl` (make sure to use the version matching your python version)
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib0.4.20cp38cp38win_amd64.whl` (make sure to use the version matching your python version).
Freqtrade provides these dependencies for the latest 2 Python versions (3.7 and 3.8) and for 64bit Windows.
Other versions must be downloaded from the above link.

View File

@@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = '2021.4'
__version__ = '2021.5'
if __version__ == 'develop':

View File

@@ -7,6 +7,7 @@ from colorama import init as colorama_init
from freqtrade.configuration import setup_utils_configuration
from freqtrade.data.btanalysis import get_latest_hyperopt_file
from freqtrade.exceptions import OperationalException
from freqtrade.optimize.optimize_reports import show_backtest_result
from freqtrade.state import RunMode
@@ -125,6 +126,12 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
if epochs:
val = epochs[n]
metrics = val['results_metrics']
if 'strategy_name' in metrics:
show_backtest_result(metrics['strategy_name'], metrics,
metrics['stake_currency'])
HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header,
header_str="Epoch details")
@@ -132,11 +139,13 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
"""
Filter our items from the list of hyperopt results
TODO: after 2021.5 remove all "legacy" mode queries.
"""
if filteroptions['only_best']:
epochs = [x for x in epochs if x['is_best']]
if filteroptions['only_profitable']:
epochs = [x for x in epochs if x['results_metrics']['profit'] > 0]
epochs = [x for x in epochs if x['results_metrics'].get(
'profit', x['results_metrics'].get('profit_total', 0)) > 0]
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
@@ -153,34 +162,55 @@ def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
return epochs
def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int):
"""
Filter epochs with trade-counts > trades
"""
return [
x for x in epochs
if x['results_metrics'].get(
'trade_count', x['results_metrics'].get('total_trades', 0)
) > trade_count
]
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_trades'] > 0:
epochs = [
x for x in epochs
if x['results_metrics']['trade_count'] > filteroptions['filter_min_trades']
]
epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades'])
if filteroptions['filter_max_trades'] > 0:
epochs = [
x for x in epochs
if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades']
if x['results_metrics'].get(
'trade_count', x['results_metrics'].get('total_trades')
) < filteroptions['filter_max_trades']
]
return epochs
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
def get_duration_value(x):
# Duration in minutes ...
if 'duration' in x['results_metrics']:
return x['results_metrics']['duration']
else:
# New mode
avg = x['results_metrics']['holding_avg']
return avg.total_seconds() // 60
if filteroptions['filter_min_avg_time'] is not None:
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics']['duration'] > filteroptions['filter_min_avg_time']
if get_duration_value(x) > filteroptions['filter_min_avg_time']
]
if filteroptions['filter_max_avg_time'] is not None:
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time']
if get_duration_value(x) < filteroptions['filter_max_avg_time']
]
return epochs
@@ -189,28 +219,36 @@ def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_avg_profit'] is not None:
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics']['avg_profit'] > filteroptions['filter_min_avg_profit']
if x['results_metrics'].get(
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
) > filteroptions['filter_min_avg_profit']
]
if filteroptions['filter_max_avg_profit'] is not None:
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics']['avg_profit'] < filteroptions['filter_max_avg_profit']
if x['results_metrics'].get(
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
) < filteroptions['filter_max_avg_profit']
]
if filteroptions['filter_min_total_profit'] is not None:
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics']['profit'] > filteroptions['filter_min_total_profit']
if x['results_metrics'].get(
'profit', x['results_metrics'].get('profit_total_abs', 0)
) > filteroptions['filter_min_total_profit']
]
if filteroptions['filter_max_total_profit'] is not None:
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit']
if x['results_metrics'].get(
'profit', x['results_metrics'].get('profit_total_abs', 0)
) < filteroptions['filter_max_total_profit']
]
return epochs
@@ -218,11 +256,11 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_objective'] is not None:
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
if filteroptions['filter_max_objective'] is not None:
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]

View File

@@ -3,6 +3,7 @@ This module contains the argument manager class
"""
import logging
import re
from datetime import datetime
from typing import Optional
import arrow
@@ -43,7 +44,7 @@ class TimeRange:
self.startts = self.startts - seconds
def adjust_start_if_necessary(self, timeframe_secs: int, startup_candles: int,
min_date: arrow.Arrow) -> None:
min_date: datetime) -> None:
"""
Adjust startts by <startup_candles> candles.
Applies only if no startup-candles have been available.
@@ -54,11 +55,11 @@ class TimeRange:
:return: None (Modifies the object in place)
"""
if (not self.starttype or (startup_candles
and min_date.int_timestamp >= self.startts)):
and min_date.timestamp() >= self.startts)):
# If no startts was defined, or backtest-data starts at the defined backtest-date
logger.warning("Moving start-date by %s candles to account for startup time.",
startup_candles)
self.startts = (min_date.int_timestamp + timeframe_secs * startup_candles)
self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles)
self.starttype = 'date'
@staticmethod

View File

@@ -11,6 +11,7 @@ DEFAULT_EXCHANGE = 'bittrex'
PROCESS_THROTTLE_SECS = 5 # sec
HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec
TIMEOUT_UNITS = ['minutes', 'seconds']
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
UNLIMITED_STAKE_AMOUNT = 'unlimited'
@@ -137,7 +138,8 @@ CONF_SCHEMA = {
'type': 'object',
'properties': {
'buy': {'type': 'number', 'minimum': 1},
'sell': {'type': 'number', 'minimum': 1}
'sell': {'type': 'number', 'minimum': 1},
'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'}
}
},
'bid_strategy': {

View File

@@ -156,33 +156,35 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
data = data['strategy'][strategy]['trades']
df = pd.DataFrame(data)
df['open_date'] = pd.to_datetime(df['open_date'],
utc=True,
infer_datetime_format=True
)
df['close_date'] = pd.to_datetime(df['close_date'],
utc=True,
infer_datetime_format=True
)
if not df.empty:
df['open_date'] = pd.to_datetime(df['open_date'],
utc=True,
infer_datetime_format=True
)
df['close_date'] = pd.to_datetime(df['close_date'],
utc=True,
infer_datetime_format=True
)
else:
# old format - only with lists.
df = pd.DataFrame(data, columns=BT_DATA_COLUMNS_OLD)
df['open_date'] = pd.to_datetime(df['open_date'],
unit='s',
utc=True,
infer_datetime_format=True
)
df['close_date'] = pd.to_datetime(df['close_date'],
unit='s',
utc=True,
infer_datetime_format=True
)
# Create compatibility with new format
df['profit_abs'] = df['close_rate'] - df['open_rate']
if 'profit_ratio' not in df.columns:
df['profit_ratio'] = df['profit_percent']
df = df.sort_values("open_date").reset_index(drop=True)
if not df.empty:
df['open_date'] = pd.to_datetime(df['open_date'],
unit='s',
utc=True,
infer_datetime_format=True
)
df['close_date'] = pd.to_datetime(df['close_date'],
unit='s',
utc=True,
infer_datetime_format=True
)
# Create compatibility with new format
df['profit_abs'] = df['close_rate'] - df['open_rate']
if not df.empty:
if 'profit_ratio' not in df.columns:
df['profit_ratio'] = df['profit_percent']
df = df.sort_values("open_date").reset_index(drop=True)
return df

View File

@@ -145,6 +145,27 @@ def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date',
return df
def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange,
startup_candles: int) -> Dict[str, DataFrame]:
"""
Trim startup period from analyzed dataframes
:param preprocessed: Dict of pair: dataframe
:param timerange: timerange (use start and end date if available)
:param startup_candles: Startup-candles that should be removed
:return: Dict of trimmed dataframes
"""
processed: Dict[str, DataFrame] = {}
for pair, df in preprocessed.items():
trimed_df = trim_dataframe(df, timerange, startup_candles=startup_candles)
if not trimed_df.empty:
processed[pair] = trimed_df
else:
logger.warning(f'{pair} has no data left after adjusting for startup candles, '
f'skipping.')
return processed
def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
"""
TODO: This should get a dedicated test

View File

@@ -19,14 +19,25 @@ from freqtrade.state import RunMode
logger = logging.getLogger(__name__)
NO_EXCHANGE_EXCEPTION = 'Exchange is not available to DataProvider.'
MAX_DATAFRAME_CANDLES = 1000
class DataProvider:
def __init__(self, config: dict, exchange: Exchange, pairlists=None) -> None:
def __init__(self, config: dict, exchange: Optional[Exchange], pairlists=None) -> None:
self._config = config
self._exchange = exchange
self._pairlists = pairlists
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
self.__slice_index: Optional[int] = None
def _set_dataframe_max_index(self, limit_index: int):
"""
Limit analyzed dataframe to max specified index.
:param limit_index: dataframe index.
"""
self.__slice_index = limit_index
def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None:
"""
@@ -45,40 +56,6 @@ class DataProvider:
"""
self._pairlists = pairlists
def refresh(self,
pairlist: ListPairsWithTimeframes,
helping_pairs: ListPairsWithTimeframes = None) -> None:
"""
Refresh data, called with each cycle
"""
if helping_pairs:
self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs)
else:
self._exchange.refresh_latest_ohlcv(pairlist)
@property
def available_pairs(self) -> ListPairsWithTimeframes:
"""
Return a list of tuples containing (pair, timeframe) for which data is currently cached.
Should be whitelist + open trades.
"""
return list(self._exchange._klines.keys())
def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame:
"""
Get candle (OHLCV) data for the given pair as DataFrame
Please use the `available_pairs` method to verify which pairs are currently cached.
:param pair: pair to get the data for
:param timeframe: Timeframe to get data for
:param copy: copy dataframe before returning if True.
Use False only for read-only operations (where the dataframe is not modified)
"""
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
return self._exchange.klines((pair, timeframe or self._config['timeframe']),
copy=copy)
else:
return DataFrame()
def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame:
"""
Get stored historical candle (OHLCV) data
@@ -111,47 +88,27 @@ class DataProvider:
def get_analyzed_dataframe(self, pair: str, timeframe: str) -> Tuple[DataFrame, datetime]:
"""
Retrieve the analyzed dataframe. Returns the full dataframe in trade mode (live / dry),
and the last 1000 candles (up to the time evaluated at this moment) in all other modes.
:param pair: pair to get the data for
:param timeframe: timeframe to get data for
:return: Tuple of (Analyzed Dataframe, lastrefreshed) for the requested pair / timeframe
combination.
Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached.
"""
if (pair, timeframe) in self.__cached_pairs:
return self.__cached_pairs[(pair, timeframe)]
pair_key = (pair, timeframe)
if pair_key in self.__cached_pairs:
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
df, date = self.__cached_pairs[pair_key]
else:
df, date = self.__cached_pairs[pair_key]
if self.__slice_index is not None:
max_index = self.__slice_index
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES):max_index]
return df, date
else:
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
def market(self, pair: str) -> Optional[Dict[str, Any]]:
"""
Return market data for the pair
:param pair: Pair to get the data for
:return: Market data dict from ccxt or None if market info is not available for the pair
"""
return self._exchange.markets.get(pair)
def ticker(self, pair: str):
"""
Return last ticker data from exchange
:param pair: Pair to get the data for
:return: Ticker dict from exchange or empty dict if ticker is not available for the pair
"""
try:
return self._exchange.fetch_ticker(pair)
except ExchangeError:
return {}
def orderbook(self, pair: str, maximum: int) -> Dict[str, List]:
"""
Fetch latest l2 orderbook data
Warning: Does a network request - so use with common sense.
:param pair: pair to get the data for
:param maximum: Maximum number of orderbook entries to query
:return: dict including bids/asks with a total of `maximum` entries.
"""
return self._exchange.fetch_l2_order_book(pair, maximum)
@property
def runmode(self) -> RunMode:
"""
@@ -173,3 +130,86 @@ class DataProvider:
return self._pairlists.whitelist.copy()
else:
raise OperationalException("Dataprovider was not initialized with a pairlist provider.")
def clear_cache(self):
"""
Clear pair dataframe cache.
"""
self.__cached_pairs = {}
# Exchange functions
def refresh(self,
pairlist: ListPairsWithTimeframes,
helping_pairs: ListPairsWithTimeframes = None) -> None:
"""
Refresh data, called with each cycle
"""
if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION)
if helping_pairs:
self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs)
else:
self._exchange.refresh_latest_ohlcv(pairlist)
@property
def available_pairs(self) -> ListPairsWithTimeframes:
"""
Return a list of tuples containing (pair, timeframe) for which data is currently cached.
Should be whitelist + open trades.
"""
if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION)
return list(self._exchange._klines.keys())
def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame:
"""
Get candle (OHLCV) data for the given pair as DataFrame
Please use the `available_pairs` method to verify which pairs are currently cached.
:param pair: pair to get the data for
:param timeframe: Timeframe to get data for
:param copy: copy dataframe before returning if True.
Use False only for read-only operations (where the dataframe is not modified)
"""
if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION)
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
return self._exchange.klines((pair, timeframe or self._config['timeframe']),
copy=copy)
else:
return DataFrame()
def market(self, pair: str) -> Optional[Dict[str, Any]]:
"""
Return market data for the pair
:param pair: Pair to get the data for
:return: Market data dict from ccxt or None if market info is not available for the pair
"""
if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION)
return self._exchange.markets.get(pair)
def ticker(self, pair: str):
"""
Return last ticker data from exchange
:param pair: Pair to get the data for
:return: Ticker dict from exchange or empty dict if ticker is not available for the pair
"""
if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION)
try:
return self._exchange.fetch_ticker(pair)
except ExchangeError:
return {}
def orderbook(self, pair: str, maximum: int) -> Dict[str, List]:
"""
Fetch latest l2 orderbook data
Warning: Does a network request - so use with common sense.
:param pair: pair to get the data for
:param maximum: Maximum number of orderbook entries to query
:return: dict including bids/asks with a total of `maximum` entries.
"""
if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION)
return self._exchange.fetch_l2_order_book(pair, maximum)

View File

@@ -265,9 +265,13 @@ def _download_trades_history(exchange: Exchange,
"""
try:
since = timerange.startts * 1000 if \
(timerange and timerange.starttype == 'date') else int(arrow.utcnow().shift(
days=-new_pairs_days).float_timestamp) * 1000
until = None
if (timerange and timerange.starttype == 'date'):
since = timerange.startts * 1000
if timerange.stoptype == 'date':
until = timerange.stopts * 1000
else:
since = int(arrow.utcnow().shift(days=-new_pairs_days).float_timestamp) * 1000
trades = data_handler.trades_load(pair)
@@ -295,6 +299,7 @@ def _download_trades_history(exchange: Exchange,
# Default since_ms to 30 days if nothing is given
new_trades = exchange.get_historic_trades(pair=pair,
since=since,
until=until,
from_id=from_id,
)
trades.extend(new_trades[1])
@@ -367,7 +372,7 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
logger.exception(f'Could not convert {pair} to OHLCV.')
def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
def get_timerange(data: Dict[str, DataFrame]) -> Tuple[datetime, datetime]:
"""
Get the maximum common timerange for the given backtest data.
@@ -375,7 +380,7 @@ def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]
:return: tuple containing min_date, max_date
"""
timeranges = [
(arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
(frame['date'].min().to_pydatetime(), frame['date'].max().to_pydatetime())
for frame in data.values()
]
return (min(timeranges, key=operator.itemgetter(0))[0],

View File

@@ -1,6 +1,8 @@
# pragma pylint: disable=W0603
""" Edge positioning package """
import logging
from collections import defaultdict
from copy import deepcopy
from typing import Any, Dict, List, NamedTuple
import arrow
@@ -12,8 +14,10 @@ from freqtrade.configuration import TimeRange
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT
from freqtrade.data.history import get_timerange, load_data, refresh_data
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.exchange import timeframe_to_seconds
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.strategy.interface import SellType
from freqtrade.state import RunMode
from freqtrade.strategy.interface import IStrategy, SellType
logger = logging.getLogger(__name__)
@@ -45,7 +49,7 @@ class Edge:
self.config = config
self.exchange = exchange
self.strategy = strategy
self.strategy: IStrategy = strategy
self.edge_config = self.config.get('edge', {})
self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
@@ -102,14 +106,33 @@ class Edge:
logger.info('Using local backtesting data (using whitelist in given config) ...')
if self._refresh_pairs:
timerange_startup = deepcopy(self._timerange)
timerange_startup.subtract_start(timeframe_to_seconds(
self.strategy.timeframe) * self.strategy.startup_candle_count)
refresh_data(
datadir=self.config['datadir'],
pairs=pairs,
exchange=self.exchange,
timeframe=self.strategy.timeframe,
timerange=self._timerange,
timerange=timerange_startup,
data_format=self.config.get('dataformat_ohlcv', 'json'),
)
# Download informative pairs too
res = defaultdict(list)
for p, t in self.strategy.informative_pairs():
res[t].append(p)
for timeframe, inf_pairs in res.items():
timerange_startup = deepcopy(self._timerange)
timerange_startup.subtract_start(timeframe_to_seconds(
timeframe) * self.strategy.startup_candle_count)
refresh_data(
datadir=self.config['datadir'],
pairs=inf_pairs,
exchange=self.exchange,
timeframe=timeframe,
timerange=timerange_startup,
data_format=self.config.get('dataformat_ohlcv', 'json'),
)
data = load_data(
datadir=self.config['datadir'],
@@ -125,8 +148,11 @@ class Edge:
self._cached_pairs = {}
logger.critical("No data found. Edge is stopped ...")
return False
# Fake run-mode to Edge
prior_rm = self.config['runmode']
self.config['runmode'] = RunMode.EDGE
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
self.config['runmode'] = prior_rm
# Print timeframe
min_date, max_date = get_timerange(preprocessed)
@@ -183,7 +209,7 @@ class Edge:
if pair in self._cached_pairs:
return self._cached_pairs[pair].stoploss
else:
logger.warning('tried to access stoploss of a non-existing pair, '
logger.warning(f'Tried to access stoploss of non-existing pair {pair}, '
'strategy stoploss is returned instead.')
return self.strategy.stoploss
@@ -214,7 +240,7 @@ class Edge:
return self._final_pairs
def accepted_pairs(self) -> list:
def accepted_pairs(self) -> List[Dict[str, Any]]:
"""
return a list of accepted pairs along with their winrate, expectancy and stoploss
"""

View File

@@ -7,6 +7,7 @@ from freqtrade.exchange.bibox import Bibox
from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.bybit import Bybit
from freqtrade.exchange.coinbasepro import Coinbasepro
from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
is_exchange_known_ccxt, is_exchange_officially_supported,
market_is_active, timeframe_to_minutes, timeframe_to_msecs,
@@ -14,5 +15,6 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
timeframe_to_seconds, validate_exchange,
validate_exchanges)
from freqtrade.exchange.ftx import Ftx
from freqtrade.exchange.hitbtc import Hitbtc
from freqtrade.exchange.kraken import Kraken
from freqtrade.exchange.kucoin import Kucoin

View File

@@ -18,7 +18,6 @@ class Bybit(Exchange):
may still not work as expected.
"""
# fetchCurrencies API point requires authentication for Bybit,
_ft_has: Dict = {
"ohlcv_candle_limit": 200,
}

View File

@@ -0,0 +1,23 @@
""" CoinbasePro exchange subclass """
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Coinbasepro(Exchange):
"""
CoinbasePro exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
_ft_has: Dict = {
"ohlcv_candle_limit": 300,
}

View File

@@ -59,6 +59,7 @@ class Exchange:
_ft_has_default: Dict = {
"stoploss_on_exchange": False,
"order_time_in_force": ["gtc"],
"ohlcv_params": {},
"ohlcv_candle_limit": 500,
"ohlcv_partial_candle": True,
"trades_pagination": "time", # Possible are "time" or "id"
@@ -465,7 +466,7 @@ class Exchange:
def amount_to_precision(self, pair: str, amount: float) -> float:
'''
Returns the amount to buy or sell to a precision the Exchange accepts
Reimplementation of ccxt internal methods - ensuring we can test the result is correct
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions.
'''
if self.markets[pair]['precision']['amount']:
@@ -479,7 +480,7 @@ class Exchange:
def price_to_precision(self, pair: str, price: float) -> float:
'''
Returns the price rounded up to the precision the Exchange accepts.
Partial Reimplementation of ccxt internal method decimal_to_precision(),
Partial Re-implementation of ccxt internal method decimal_to_precision(),
which does not support rounding up
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
align with amount_to_precision().
@@ -862,10 +863,11 @@ class Exchange:
"Fetching pair %s, interval %s, since %s %s...",
pair, timeframe, since_ms, s
)
params = self._ft_has.get('ohlcv_params', {})
data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe,
since=since_ms,
limit=self.ohlcv_candle_limit(timeframe))
limit=self.ohlcv_candle_limit(timeframe),
params=params)
# Some exchanges sort OHLCV in ASC order and others in DESC.
# Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
@@ -1118,6 +1120,27 @@ class Exchange:
return order
def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
"""
Cancel stoploss order returning a result.
Creates a fake result if cancel order returns a non-usable result
and fetch_order does not work (certain exchanges don't return cancelled orders)
:param order_id: stoploss-order-id to cancel
:param pair: Pair corresponding to order_id
:param amount: Amount to use for fake response
:return: Result from either cancel_order if usable, or fetch_order
"""
corder = self.cancel_stoploss_order(order_id, pair)
if self.is_cancel_order_result_suitable(corder):
return corder
try:
order = self.fetch_stoploss_order(order_id, pair)
except InvalidOrderException:
logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
return order
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
def fetch_order(self, order_id: str, pair: str) -> Dict:
if self._config['dry_run']:
@@ -1237,6 +1260,9 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
return order['id']
@retrier
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
price: float = 1, taker_or_maker: str = 'maker') -> float:

View File

@@ -8,6 +8,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier
from freqtrade.misc import safe_value_fallback2
logger = logging.getLogger(__name__)
@@ -135,3 +136,8 @@ class Ftx(Exchange):
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
if order['type'] == 'stop':
return safe_value_fallback2(order['info'], order, 'orderId', 'id')
return order['id']

View File

@@ -0,0 +1,23 @@
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Hitbtc(Exchange):
"""
Hitbtc exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
_ft_has: Dict = {
"ohlcv_candle_limit": 1000,
"ohlcv_params": {"sort": "DESC"}
}

View File

@@ -53,6 +53,8 @@ class Kraken(Exchange):
# x["side"], x["amount"],
) for x in orders]
for bal in balances:
if not isinstance(balances[bal], dict):
continue
balances[bal]['used'] = sum(order[1] for order in order_list if order[0] == bal)
balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used']

View File

@@ -28,7 +28,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.state import State
from freqtrade.strategy.interface import IStrategy, SellType
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets
@@ -267,7 +267,7 @@ class FreqtradeBot(LoggingMixin):
def update_closed_trades_without_assigned_fees(self):
"""
Update closed trades without close fees assigned.
Only acts when Orders are in the database, otherwise the last orderid is unknown.
Only acts when Orders are in the database, otherwise the last order-id is unknown.
"""
if self.config['dry_run']:
# Updating open orders in dry-run does not make sense and will fail.
@@ -552,7 +552,7 @@ class FreqtradeBot(LoggingMixin):
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested,
time_in_force=time_in_force):
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)):
logger.info(f"User requested abortion of buying {pair}")
return False
amount = self.exchange.amount_to_precision(pair, amount)
@@ -601,6 +601,7 @@ class FreqtradeBot(LoggingMixin):
pair=pair,
stake_amount=stake_amount,
amount=amount,
is_open=True,
amount_requested=amount_requested,
fee_open=fee,
fee_close=fee,
@@ -630,7 +631,7 @@ class FreqtradeBot(LoggingMixin):
def _notify_buy(self, trade: Trade, order_type: str) -> None:
"""
Sends rpc notification when a buy occured.
Sends rpc notification when a buy occurred.
"""
msg = {
'trade_id': trade.id,
@@ -652,7 +653,7 @@ class FreqtradeBot(LoggingMixin):
def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
"""
Sends rpc notification when a buy cancel occured.
Sends rpc notification when a buy cancel occurred.
"""
current_rate = self.get_buy_rate(trade.pair, False)
@@ -713,7 +714,7 @@ class FreqtradeBot(LoggingMixin):
except DependencyException as exception:
logger.warning('Unable to sell trade %s: %s', trade.pair, exception)
# Updating wallets if any trade occured
# Updating wallets if any trade occurred
if trades_closed:
self.wallets.update()
@@ -850,7 +851,8 @@ class FreqtradeBot(LoggingMixin):
trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Selling the trade forcefully')
self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL)
self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple(
sell_type=SellType.EMERGENCY_SELL))
except ExchangeError:
trade.stoploss_order_id = None
@@ -931,14 +933,15 @@ class FreqtradeBot(LoggingMixin):
:return: None
"""
if self.exchange.stoploss_adjust(trade.stop_loss, order):
# we check if the update is neccesary
# we check if the update is necessary
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
# cancelling the current stoploss on exchange first
logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} "
f"(orderid:{order['id']}) in order to add another one ...")
try:
co = self.exchange.cancel_stoploss_order(order['id'], trade.pair)
co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair,
trade.amount)
trade.update_order(co)
except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {order['id']} "
@@ -961,7 +964,7 @@ class FreqtradeBot(LoggingMixin):
if should_sell.sell_flag:
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
self.execute_sell(trade, sell_rate, should_sell.sell_type)
self.execute_sell(trade, sell_rate, should_sell)
return True
return False
@@ -972,15 +975,16 @@ class FreqtradeBot(LoggingMixin):
timeout = self.config.get('unfilledtimeout', {}).get(side)
ordertime = arrow.get(order['datetime']).datetime
if timeout is not None:
timeout_threshold = arrow.utcnow().shift(minutes=-timeout).datetime
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
timeout_kwargs = {timeout_unit: -timeout}
timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).datetime
return (order['status'] == 'open' and order['side'] == side
and ordertime < timeout_threshold)
return False
def check_handle_timedout(self) -> None:
"""
Check if any orders are timed out and cancel if neccessary
Check if any orders are timed out and cancel if necessary
:param timeoutvalue: Number of minutes until order is considered timed out
:return: None
"""
@@ -1042,6 +1046,16 @@ class FreqtradeBot(LoggingMixin):
# Cancelled orders may have the status of 'canceled' or 'closed'
if order['status'] not in ('cancelled', 'canceled', 'closed'):
filled_val = order.get('filled', 0.0) or 0.0
filled_stake = filled_val * trade.open_rate
minstake = self.exchange.get_min_pair_stake_amount(
trade.pair, trade.open_rate, self.strategy.stoploss)
if filled_val > 0 and filled_stake < minstake:
logger.warning(
f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
f"as the filled amount of {filled_val} would result in an unsellable trade.")
return False
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
trade.amount)
# Avoid race condition where the order could not be cancelled coz its already filled.
@@ -1150,16 +1164,16 @@ class FreqtradeBot(LoggingMixin):
raise DependencyException(
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> bool:
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
"""
Executes a limit sell for the given trade and limit
:param trade: Trade instance
:param limit: limit rate for the sell order
:param sellreason: Reason the sell was triggered
:param sell_reason: Reason the sell was triggered
:return: True if it succeeds (supported) False (not supported)
"""
sell_type = 'sell'
if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
sell_type = 'stoploss'
# if stoploss is on exchange and we are on dry_run mode,
@@ -1171,15 +1185,17 @@ class FreqtradeBot(LoggingMixin):
# First cancelling stoploss on exchange ...
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
try:
self.exchange.cancel_stoploss_order(trade.stoploss_order_id, trade.pair)
co = self.exchange.cancel_stoploss_order_with_result(trade.stoploss_order_id,
trade.pair, trade.amount)
trade.update_order(co)
except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
order_type = self.strategy.order_types[sell_type]
if sell_reason == SellType.EMERGENCY_SELL:
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
# Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergencysell", "market")
if sell_reason == SellType.FORCE_SELL:
if sell_reason.sell_type == SellType.FORCE_SELL:
# Force sells (default to the sell_type defined in the strategy,
# but we allow this value to be changed)
order_type = self.strategy.order_types.get("forcesell", order_type)
@@ -1189,8 +1205,8 @@ class FreqtradeBot(LoggingMixin):
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
time_in_force=time_in_force,
sell_reason=sell_reason.value):
time_in_force=time_in_force, sell_reason=sell_reason.sell_reason,
current_time=datetime.now(timezone.utc)):
logger.info(f"User requested abortion of selling {trade.pair}")
return False
@@ -1213,13 +1229,13 @@ class FreqtradeBot(LoggingMixin):
trade.open_order_id = order['id']
trade.sell_order_status = ''
trade.close_rate_requested = limit
trade.sell_reason = sell_reason.value
trade.sell_reason = sell_reason.sell_reason
# In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') == 'closed':
self.update_trade_state(trade, trade.open_order_id, order)
Trade.query.session.flush()
# Lock pair for one candle to prevent immediate rebuys
# Lock pair for one candle to prevent immediate re-buys
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
reason='Auto lock')
@@ -1229,7 +1245,7 @@ class FreqtradeBot(LoggingMixin):
def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None:
"""
Sends rpc notification when a sell occured.
Sends rpc notification when a sell occurred.
"""
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate)
@@ -1270,7 +1286,7 @@ class FreqtradeBot(LoggingMixin):
def _notify_sell_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
"""
Sends rpc notification when a sell cancel occured.
Sends rpc notification when a sell cancel occurred.
"""
if trade.sell_order_status == reason:
return
@@ -1323,7 +1339,7 @@ class FreqtradeBot(LoggingMixin):
Handles closing both buy and sell orders.
:param trade: Trade object of the trade we're analyzing
:param order_id: Order-id of the order we're analyzing
:param action_order: Already aquired order object
:param action_order: Already acquired order object
:return: True if order has been cancelled without being filled partially, False otherwise
"""
if not order_id:
@@ -1393,7 +1409,7 @@ class FreqtradeBot(LoggingMixin):
def get_real_amount(self, trade: Trade, order: Dict) -> float:
"""
Detect and update trade fee.
Calls trade.update_fee() uppon correct detection.
Calls trade.update_fee() upon correct detection.
Returns modified amount if the fee was taken from the destination currency.
Necessary for exchanges which charge fees in base currency (e.g. binance)
:return: identical (or new) amount for the trade
@@ -1426,8 +1442,8 @@ class FreqtradeBot(LoggingMixin):
"""
fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee.
"""
trades = self.exchange.get_trades_for_order(order['id'], trade.pair,
trade.open_date)
trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order),
trade.pair, trade.open_date)
if len(trades) == 0:
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)

View File

@@ -6,7 +6,7 @@ import logging
import re
from datetime import datetime
from pathlib import Path
from typing import Any
from typing import Any, Iterator, List
from typing.io import IO
import rapidjson
@@ -202,3 +202,14 @@ def render_template_with_fallback(templatefile: str, templatefallbackfile: str,
return render_template(templatefile, arguments)
except TemplateNotFound:
return render_template(templatefallbackfile, arguments)
def chunks(lst: List[Any], n: int) -> Iterator[List[Any]]:
"""
Split lst into chunks of the size n.
:param lst: list to split into chunks
:param n: number of max elements per chunk
:return: None
"""
for chunk in range(0, len(lst), n):
yield (lst[chunk:chunk + n])

View File

@@ -15,7 +15,7 @@ from freqtrade.configuration import TimeRange, remove_credentials, validate_conf
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.data import history
from freqtrade.data.btanalysis import trade_list_to_dataframe
from freqtrade.data.converter import trim_dataframe
from freqtrade.data.converter import trim_dataframes
from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
@@ -63,9 +63,7 @@ class Backtesting:
self.all_results: Dict[str, Dict] = {}
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
dataprovider = DataProvider(self.config, self.exchange)
IStrategy.dp = dataprovider
self.dataprovider = DataProvider(self.config, None)
if self.config.get('strategy_list', None):
for strat in list(self.config['strategy_list']):
@@ -96,7 +94,7 @@ class Backtesting:
"PrecisionFilter not allowed for backtesting multiple strategies."
)
dataprovider.add_pairlisthandler(self.pairlists)
self.dataprovider.add_pairlisthandler(self.pairlists)
self.pairlists.refresh_pairlist()
if len(self.pairlists.whitelist) == 0:
@@ -112,15 +110,11 @@ class Backtesting:
PairLocks.timeframe = self.config['timeframe']
PairLocks.use_db = False
PairLocks.reset_locks()
if self.config.get('enable_protections', False):
self.protections = ProtectionManager(self.config)
self.wallets = Wallets(self.config, self.exchange, log=False)
# Get maximum required startup period
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
# Load one (first) strategy
self._set_strategy(self.strategylist[0])
def __del__(self):
LoggingMixin.show_output = True
@@ -132,10 +126,17 @@ class Backtesting:
Load strategy into backtesting
"""
self.strategy: IStrategy = strategy
strategy.dp = self.dataprovider
# Set stoploss_on_exchange to false for backtesting,
# since a "perfect" stoploss-sell is assumed anyway
# And the regular "stoploss" function would not apply to that case
self.strategy.order_types['stoploss_on_exchange'] = False
if self.config.get('enable_protections', False):
conf = self.config
if hasattr(strategy, 'protections'):
conf = deepcopy(conf)
conf['protections'] = strategy.protections
self.protections = ProtectionManager(conf)
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:
"""
@@ -159,7 +160,7 @@ class Backtesting:
logger.info(f'Loading data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
f'({(max_date - min_date).days} days)..')
f'({(max_date - min_date).days} days).')
# Adjust startts forward if not enough data is available
timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
@@ -176,6 +177,8 @@ class Backtesting:
Trade.use_db = False
PairLocks.reset_locks()
Trade.reset_trades()
self.rejected_trades = 0
self.dataprovider.clear_cache()
def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]:
"""
@@ -189,8 +192,9 @@ class Backtesting:
data: Dict = {}
# Create dict with data
for pair, pair_data in processed.items():
pair_data.loc[:, 'buy'] = 0 # cleanup from previous run
pair_data.loc[:, 'sell'] = 0 # cleanup from previous run
if not pair_data.empty:
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
df_analyzed = self.strategy.advise_sell(
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
@@ -214,6 +218,12 @@ class Backtesting:
"""
# Special handling if high or low hit STOP_LOSS or ROI
if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
if trade.stop_loss > sell_row[HIGH_IDX]:
# our stoploss was already higher than candle high,
# possibly due to a cancelled trade exit.
# sell at open price.
return sell_row[OPEN_IDX]
# Set close_rate to stoploss
return trade.stop_loss
elif sell.sell_type == (SellType.ROI):
@@ -250,12 +260,13 @@ class Backtesting:
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX],
sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX],
sell_row[SELL_IDX],
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
if sell.sell_flag:
trade.close_date = sell_row[DATE_IDX]
trade.sell_reason = sell.sell_type.value
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
trade.sell_reason = sell.sell_reason
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
@@ -265,7 +276,8 @@ class Backtesting:
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
rate=closerate,
time_in_force=time_in_force,
sell_reason=sell.sell_type.value):
sell_reason=sell.sell_reason,
current_time=sell_row[DATE_IDX].to_pydatetime()):
return None
trade.close(closerate, show_msg=False)
@@ -285,7 +297,7 @@ class Backtesting:
# Confirm trade entry:
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX],
time_in_force=time_in_force):
time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()):
return None
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
@@ -293,7 +305,7 @@ class Backtesting:
trade = LocalTrade(
pair=pair,
open_rate=row[OPEN_IDX],
open_date=row[DATE_IDX],
open_date=row[DATE_IDX].to_pydatetime(),
stake_amount=stake_amount,
amount=round(stake_amount / row[OPEN_IDX], 8),
fee_open=self.fee,
@@ -315,7 +327,7 @@ class Backtesting:
for trade in open_trades[pair]:
sell_row = data[pair][-1]
trade.close_date = sell_row[DATE_IDX]
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
trade.sell_reason = SellType.FORCE_SELL.value
trade.close(sell_row[OPEN_IDX], show_msg=False)
LocalTrade.close_bt_trade(trade)
@@ -325,10 +337,18 @@ class Backtesting:
trades.append(trade1)
return trades
def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool:
# Always allow trades when max_open_trades is enabled.
if max_open_trades <= 0 or open_trade_count < max_open_trades:
return True
# Rejected trade
self.rejected_trades += 1
return False
def backtest(self, processed: Dict,
start_date: datetime, end_date: datetime,
max_open_trades: int = 0, position_stacking: bool = False,
enable_protections: bool = False) -> DataFrame:
enable_protections: bool = False) -> Dict[str, Any]:
"""
Implement backtesting functionality
@@ -347,6 +367,10 @@ class Backtesting:
trades: List[LocalTrade] = []
self.prepare_backtest(enable_protections)
# Update dataprovider cache
for pair, dataframe in processed.items():
self.dataprovider._set_cached_df(pair, self.timeframe, dataframe)
# Use dict of lists with data for performance
# (looping lists is a lot faster than pandas DataFrames)
data: Dict = self._get_ohlcv_as_lists(processed)
@@ -363,8 +387,9 @@ class Backtesting:
open_trade_count_start = open_trade_count
for i, pair in enumerate(data):
row_index = indexes[pair]
try:
row = data[pair][indexes[pair]]
row = data[pair][row_index]
except IndexError:
# missing Data for one pair at the end.
# Warnings for this are shown during data loading
@@ -373,16 +398,22 @@ class Backtesting:
# Waits until the time-counter reaches the start of the data for this pair.
if row[DATE_IDX] > tmp:
continue
indexes[pair] += 1
row_index += 1
self.dataprovider._set_dataframe_max_index(row_index)
indexes[pair] = row_index
# without positionstacking, we can only have one open trade per pair.
# max_open_trades must be respected
# don't open on the last row
if ((position_stacking or len(open_trades[pair]) == 0)
and (max_open_trades <= 0 or open_trade_count_start < max_open_trades)
and tmp != end_date
and row[BUY_IDX] == 1 and row[SELL_IDX] != 1
and not PairLocks.is_pair_locked(pair, row[DATE_IDX])):
if (
(position_stacking or len(open_trades[pair]) == 0)
and self.trade_slot_available(max_open_trades, open_trade_count_start)
and tmp != end_date
and row[BUY_IDX] == 1
and row[SELL_IDX] != 1
and not PairLocks.is_pair_locked(pair, row[DATE_IDX])
):
trade = self._enter_trade(pair, row)
if trade:
# TODO: hacky workaround to avoid opening > max_open_trades
@@ -415,7 +446,14 @@ class Backtesting:
trades += self.handle_left_open(open_trades, data=data)
self.wallets.update()
return trade_list_to_dataframe(trades)
results = trade_list_to_dataframe(trades)
return {
'results': results,
'config': self.strategy.config,
'locks': PairLocks.get_all_locks(),
'rejected_signals': self.rejected_trades,
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
}
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange):
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
@@ -437,32 +475,32 @@ class Backtesting:
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
# Trim startup period from analyzed dataframe
for pair, df in preprocessed.items():
preprocessed[pair] = trim_dataframe(df, timerange,
startup_candles=self.required_startup)
min_date, max_date = history.get_timerange(preprocessed)
preprocessed = trim_dataframes(preprocessed, timerange, self.required_startup)
if not preprocessed:
raise OperationalException(
"No data left after adjusting for startup candles.")
min_date, max_date = history.get_timerange(preprocessed)
logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
f'({(max_date - min_date).days} days)..')
f'({(max_date - min_date).days} days).')
# Execute backtest and store results
results = self.backtest(
processed=preprocessed,
start_date=min_date.datetime,
end_date=max_date.datetime,
start_date=min_date,
end_date=max_date,
max_open_trades=max_open_trades,
position_stacking=self.config.get('position_stacking', False),
enable_protections=self.config.get('enable_protections', False),
)
backtest_end_time = datetime.now(timezone.utc)
self.all_results[self.strategy.get_strategy_name()] = {
'results': results,
'config': self.strategy.config,
'locks': PairLocks.get_all_locks(),
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
results.update({
'backtest_start_time': int(backtest_start_time.timestamp()),
'backtest_end_time': int(backtest_end_time.timestamp()),
}
})
self.all_results[self.strategy.get_strategy_name()] = results
return min_date, max_date
def start(self) -> None:

View File

@@ -4,24 +4,24 @@
This module contains the hyperopt logic
"""
import locale
import logging
import random
import warnings
from datetime import datetime
from datetime import datetime, timezone
from math import ceil
from operator import itemgetter
from pathlib import Path
from typing import Any, Dict, List, Optional
import numpy as np
import progressbar
import rapidjson
from colorama import Fore, Style
from colorama import init as colorama_init
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
from pandas import DataFrame
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
from freqtrade.data.converter import trim_dataframe
from freqtrade.data.converter import trim_dataframes
from freqtrade.data.history import get_timerange
from freqtrade.misc import file_dump_json, plural
from freqtrade.optimize.backtesting import Backtesting
@@ -30,8 +30,8 @@ from freqtrade.optimize.hyperopt_auto import HyperOptAuto
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.optimize.optimize_reports import generate_strategy_stats
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
from freqtrade.strategy import IStrategy
# Suppress scikit-learn FutureWarnings from skopt
@@ -65,6 +65,13 @@ class Hyperopt:
custom_hyperopt: IHyperOpt
def __init__(self, config: Dict[str, Any]) -> None:
self.buy_space: List[Dimension] = []
self.sell_space: List[Dimension] = []
self.roi_space: List[Dimension] = []
self.stoploss_space: List[Dimension] = []
self.trailing_space: List[Dimension] = []
self.dimensions: List[Dimension] = []
self.config = config
self.backtesting = Backtesting(self.config)
@@ -73,15 +80,15 @@ class Hyperopt:
self.custom_hyperopt = HyperOptAuto(self.config)
else:
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
self.backtesting._set_strategy(self.backtesting.strategylist[0])
self.custom_hyperopt.strategy = self.backtesting.strategy
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
strategy = str(self.config['strategy'])
self.results_file = (self.config['user_data_dir'] /
'hyperopt_results' /
f'strategy_{strategy}_hyperopt_results_{time_now}.pickle')
self.results_file: Path = (self.config['user_data_dir'] / 'hyperopt_results' /
f'strategy_{strategy}_{time_now}.fthypt')
self.data_pickle_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_tickerdata.pkl')
self.total_epochs = config.get('epochs', 0)
@@ -91,9 +98,7 @@ class Hyperopt:
self.clean_hyperopt()
self.num_epochs_saved = 0
# Previous evaluations
self.epochs: List = []
self.current_best_epoch: Optional[Dict[str, Any]] = None
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
if hasattr(self.custom_hyperopt, 'populate_indicators'):
@@ -114,7 +119,7 @@ class Hyperopt:
self.max_open_trades = 0
self.position_stacking = self.config.get('position_stacking', False)
if self.has_space('sell'):
if HyperoptTools.has_space(self.config, 'sell'):
# Make sure use_sell_signal is enabled
if 'ask_strategy' not in self.config:
self.config['ask_strategy'] = {}
@@ -140,9 +145,7 @@ class Hyperopt:
logger.info(f"Removing `{p}`.")
p.unlink()
def _get_params_dict(self, raw_params: List[Any]) -> Dict:
dimensions: List[Dimension] = self.dimensions
def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict:
# Ensure the number of dimensions match
# the number of parameters in the list.
@@ -153,21 +156,30 @@ class Hyperopt:
# and the values are taken from the list of parameters.
return {d.name: v for d, v in zip(dimensions, raw_params)}
def _save_results(self) -> None:
def _save_result(self, epoch: Dict) -> None:
"""
Save hyperopt results to file
Store one line per epoch.
While not a valid json object - this allows appending easily.
:param epoch: result dictionary for this epoch.
"""
num_epochs = len(self.epochs)
if num_epochs > self.num_epochs_saved:
logger.debug(f"Saving {num_epochs} {plural(num_epochs, 'epoch')}.")
dump(self.epochs, self.results_file)
self.num_epochs_saved = num_epochs
logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
f"saved to '{self.results_file}'.")
# Store hyperopt filename
latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN)
file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)},
log=False)
def default_parser(x):
if isinstance(x, np.integer):
return int(x)
return str(x)
with self.results_file.open('a') as f:
rapidjson.dump(epoch, f, default=default_parser,
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN)
f.write("\n")
self.num_epochs_saved += 1
logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
f"saved to '{self.results_file}'.")
# Store hyperopt filename
latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN)
file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)},
log=False)
def _get_params_details(self, params: Dict) -> Dict:
"""
@@ -175,18 +187,16 @@ class Hyperopt:
"""
result: Dict = {}
if self.has_space('buy'):
result['buy'] = {p.name: params.get(p.name)
for p in self.hyperopt_space('buy')}
if self.has_space('sell'):
result['sell'] = {p.name: params.get(p.name)
for p in self.hyperopt_space('sell')}
if self.has_space('roi'):
result['roi'] = self.custom_hyperopt.generate_roi_table(params)
if self.has_space('stoploss'):
result['stoploss'] = {p.name: params.get(p.name)
for p in self.hyperopt_space('stoploss')}
if self.has_space('trailing'):
if HyperoptTools.has_space(self.config, 'buy'):
result['buy'] = {p.name: params.get(p.name) for p in self.buy_space}
if HyperoptTools.has_space(self.config, 'sell'):
result['sell'] = {p.name: params.get(p.name) for p in self.sell_space}
if HyperoptTools.has_space(self.config, 'roi'):
result['roi'] = {str(k): v for k, v in
self.custom_hyperopt.generate_roi_table(params).items()}
if HyperoptTools.has_space(self.config, 'stoploss'):
result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space}
if HyperoptTools.has_space(self.config, 'trailing'):
result['trailing'] = self.custom_hyperopt.generate_trailing_params(params)
return result
@@ -208,71 +218,58 @@ class Hyperopt:
)
self.hyperopt_table_header = 2
def has_space(self, space: str) -> bool:
def init_spaces(self):
"""
Tell if the space value is contained in the configuration
Assign the dimensions in the hyperoptimization space.
"""
# The 'trailing' space is not included in the 'default' set of spaces
if space == 'trailing':
return any(s in self.config['spaces'] for s in [space, 'all'])
else:
return any(s in self.config['spaces'] for s in [space, 'all', 'default'])
def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]:
"""
Return the dimensions in the hyperoptimization space.
:param space: Defines hyperspace to return dimensions for.
If None, then the self.has_space() will be used to return dimensions
for all hyperspaces used.
"""
spaces: List[Dimension] = []
if space == 'buy' or (space is None and self.has_space('buy')):
if HyperoptTools.has_space(self.config, 'buy'):
logger.debug("Hyperopt has 'buy' space")
spaces += self.custom_hyperopt.indicator_space()
self.buy_space = self.custom_hyperopt.indicator_space()
if space == 'sell' or (space is None and self.has_space('sell')):
if HyperoptTools.has_space(self.config, 'sell'):
logger.debug("Hyperopt has 'sell' space")
spaces += self.custom_hyperopt.sell_indicator_space()
self.sell_space = self.custom_hyperopt.sell_indicator_space()
if space == 'roi' or (space is None and self.has_space('roi')):
if HyperoptTools.has_space(self.config, 'roi'):
logger.debug("Hyperopt has 'roi' space")
spaces += self.custom_hyperopt.roi_space()
self.roi_space = self.custom_hyperopt.roi_space()
if space == 'stoploss' or (space is None and self.has_space('stoploss')):
if HyperoptTools.has_space(self.config, 'stoploss'):
logger.debug("Hyperopt has 'stoploss' space")
spaces += self.custom_hyperopt.stoploss_space()
self.stoploss_space = self.custom_hyperopt.stoploss_space()
if space == 'trailing' or (space is None and self.has_space('trailing')):
if HyperoptTools.has_space(self.config, 'trailing'):
logger.debug("Hyperopt has 'trailing' space")
spaces += self.custom_hyperopt.trailing_space()
return spaces
self.trailing_space = self.custom_hyperopt.trailing_space()
self.dimensions = (self.buy_space + self.sell_space + self.roi_space +
self.stoploss_space + self.trailing_space)
def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
"""
Used Optimize function. Called once per epoch to optimize whatever is configured.
Keep this function as optimized as possible!
"""
params_dict = self._get_params_dict(raw_params)
params_details = self._get_params_details(params_dict)
backtest_start_time = datetime.now(timezone.utc)
params_dict = self._get_params_dict(self.dimensions, raw_params)
if self.has_space('roi'):
# Apply parameters
if HyperoptTools.has_space(self.config, 'roi'):
self.backtesting.strategy.minimal_roi = ( # type: ignore
self.custom_hyperopt.generate_roi_table(params_dict))
if self.has_space('buy'):
if HyperoptTools.has_space(self.config, 'buy'):
self.backtesting.strategy.advise_buy = ( # type: ignore
self.custom_hyperopt.buy_strategy_generator(params_dict))
if self.has_space('sell'):
if HyperoptTools.has_space(self.config, 'sell'):
self.backtesting.strategy.advise_sell = ( # type: ignore
self.custom_hyperopt.sell_strategy_generator(params_dict))
if self.has_space('stoploss'):
if HyperoptTools.has_space(self.config, 'stoploss'):
self.backtesting.strategy.stoploss = params_dict['stoploss']
if self.has_space('trailing'):
if HyperoptTools.has_space(self.config, 'trailing'):
d = self.custom_hyperopt.generate_trailing_params(params_dict)
self.backtesting.strategy.trailing_stop = d['trailing_stop']
self.backtesting.strategy.trailing_stop_positive = d['trailing_stop_positive']
@@ -281,30 +278,42 @@ class Hyperopt:
self.backtesting.strategy.trailing_only_offset_is_reached = \
d['trailing_only_offset_is_reached']
processed = load(self.data_pickle_file)
min_date, max_date = get_timerange(processed)
backtesting_results = self.backtesting.backtest(
with self.data_pickle_file.open('rb') as f:
processed = load(f, mmap_mode='r')
bt_results = self.backtesting.backtest(
processed=processed,
start_date=min_date.datetime,
end_date=max_date.datetime,
start_date=self.min_date,
end_date=self.max_date,
max_open_trades=self.max_open_trades,
position_stacking=self.position_stacking,
enable_protections=self.config.get('enable_protections', False),
)
return self._get_results_dict(backtesting_results, min_date, max_date,
params_dict, params_details,
backtest_end_time = datetime.now(timezone.utc)
bt_results.update({
'backtest_start_time': int(backtest_start_time.timestamp()),
'backtest_end_time': int(backtest_end_time.timestamp()),
})
return self._get_results_dict(bt_results, self.min_date, self.max_date,
params_dict,
processed=processed)
def _get_results_dict(self, backtesting_results, min_date, max_date,
params_dict, params_details, processed: Dict[str, DataFrame]):
results_metrics = self._calculate_results_metrics(backtesting_results)
results_explanation = self._format_results_explanation_string(results_metrics)
params_dict, processed: Dict[str, DataFrame]
) -> Dict[str, Any]:
params_details = self._get_params_details(params_dict)
trade_count = results_metrics['trade_count']
total_profit = results_metrics['total_profit']
strat_stats = generate_strategy_stats(
processed, self.backtesting.strategy.get_strategy_name(),
backtesting_results, min_date, max_date, market_change=0
)
results_explanation = HyperoptTools.format_results_explanation_string(
strat_stats, self.config['stake_currency'])
not_optimized = self.backtesting.strategy.get_params_dict()
trade_count = strat_stats['total_trades']
total_profit = strat_stats['profit_total']
# If this evaluation contains too short amount of trades to be
# interesting -- consider it as 'bad' (assigned max. loss value)
@@ -312,50 +321,20 @@ class Hyperopt:
# path. We do not want to optimize 'hodl' strategies.
loss: float = MAX_LOSS
if trade_count >= self.config['hyperopt_min_trades']:
loss = self.calculate_loss(results=backtesting_results, trade_count=trade_count,
min_date=min_date.datetime, max_date=max_date.datetime,
loss = self.calculate_loss(results=backtesting_results['results'],
trade_count=trade_count,
min_date=min_date, max_date=max_date,
config=self.config, processed=processed)
return {
'loss': loss,
'params_dict': params_dict,
'params_details': params_details,
'results_metrics': results_metrics,
'params_not_optimized': not_optimized,
'results_metrics': strat_stats,
'results_explanation': results_explanation,
'total_profit': total_profit,
}
def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict:
wins = len(backtesting_results[backtesting_results['profit_ratio'] > 0])
draws = len(backtesting_results[backtesting_results['profit_ratio'] == 0])
losses = len(backtesting_results[backtesting_results['profit_ratio'] < 0])
return {
'trade_count': len(backtesting_results.index),
'wins': wins,
'draws': draws,
'losses': losses,
'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}",
'avg_profit': backtesting_results['profit_ratio'].mean() * 100.0,
'median_profit': backtesting_results['profit_ratio'].median() * 100.0,
'total_profit': backtesting_results['profit_abs'].sum(),
'profit': backtesting_results['profit_ratio'].sum() * 100.0,
'duration': backtesting_results['trade_duration'].mean(),
}
def _format_results_explanation_string(self, results_metrics: Dict) -> str:
"""
Return the formatted results explanation in a string
"""
stake_cur = self.config['stake_currency']
return (f"{results_metrics['trade_count']:6d} trades. "
f"{results_metrics['wins']}/{results_metrics['draws']}"
f"/{results_metrics['losses']} Wins/Draws/Losses. "
f"Avg profit {results_metrics['avg_profit']: 6.2f}%. "
f"Median profit {results_metrics['median_profit']: 6.2f}%. "
f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} "
f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). "
f"Avg duration {results_metrics['duration']:5.1f} min."
).encode(locale.getpreferredencoding(), 'replace').decode('utf-8')
def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer:
return Optimizer(
dimensions,
@@ -374,25 +353,31 @@ class Hyperopt:
def _set_random_state(self, random_state: Optional[int]) -> int:
return random_state or random.randint(1, 2**16 - 1)
def prepare_hyperopt_data(self) -> None:
data, timerange = self.backtesting.load_bt_data()
logger.info("Dataload complete. Calculating indicators")
preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data)
# Trim startup period from analyzed dataframe
processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup)
self.min_date, self.max_date = get_timerange(processed)
logger.info(f'Hyperopting with data from {self.min_date.strftime(DATETIME_PRINT_FORMAT)} '
f'up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} '
f'({(self.max_date - self.min_date).days} days)..')
dump(processed, self.data_pickle_file)
def start(self) -> None:
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
logger.info(f"Using optimizer random state: {self.random_state}")
self.hyperopt_table_header = -1
data, timerange = self.backtesting.load_bt_data()
logger.info("Dataload complete. Calculating indicators")
preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data)
# Initialize spaces ...
self.init_spaces()
# Trim startup period from analyzed dataframe
for pair, df in preprocessed.items():
preprocessed[pair] = trim_dataframe(df, timerange,
startup_candles=self.backtesting.required_startup)
min_date, max_date = get_timerange(preprocessed)
logger.info(f'Hyperopting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
f'({(max_date - min_date).days} days)..')
dump(preprocessed, self.data_pickle_file)
self.prepare_hyperopt_data()
# We don't need exchange instance anymore while running hyperopt
self.backtesting.exchange.close()
@@ -400,15 +385,12 @@ class Hyperopt:
self.backtesting.exchange._api_async = None # type: ignore
# self.backtesting.exchange = None # type: ignore
self.backtesting.pairlists = None # type: ignore
self.backtesting.strategy.dp = None # type: ignore
IStrategy.dp = None # type: ignore
cpus = cpu_count()
logger.info(f"Found {cpus} CPU cores. Let's make them scream!")
config_jobs = self.config.get('hyperopt_jobs', -1)
logger.info(f'Number of parallel jobs set as: {config_jobs}')
self.dimensions: List[Dimension] = self.hyperopt_space()
self.opt = self.get_optimizer(self.dimensions, config_jobs)
if self.print_colorized:
@@ -474,25 +456,21 @@ class Hyperopt:
if is_best:
self.current_best_loss = val['loss']
self.epochs.append(val)
self.current_best_epoch = val
# Save results after each best epoch and every 100 epochs
if is_best or current % 100 == 0:
self._save_results()
self._save_result(val)
pbar.update(current)
except KeyboardInterrupt:
print('User interrupted..')
self._save_results()
logger.info(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
f"saved to '{self.results_file}'.")
if self.epochs:
sorted_epochs = sorted(self.epochs, key=itemgetter('loss'))
best_epoch = sorted_epochs[0]
HyperoptTools.print_epoch_details(best_epoch, self.total_epochs, self.print_json)
if self.current_best_epoch:
HyperoptTools.print_epoch_details(self.current_best_epoch, self.total_epochs,
self.print_json)
else:
# This is printed when Ctrl+C is pressed quickly, before first epochs have
# a chance to be evaluated.

View File

@@ -1,19 +1,18 @@
import io
import locale
import logging
from collections import OrderedDict
from pathlib import Path
from pprint import pformat
from typing import Dict, List
from typing import Any, Dict, List
import rapidjson
import tabulate
from colorama import Fore, Style
from joblib import load
from pandas import isna, json_normalize
from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_dict
from freqtrade.misc import round_coin_value, round_dict
logger = logging.getLogger(__name__)
@@ -21,13 +20,38 @@ logger = logging.getLogger(__name__)
class HyperoptTools():
@staticmethod
def has_space(config: Dict[str, Any], space: str) -> bool:
"""
Tell if the space value is contained in the configuration
"""
# The 'trailing' space is not included in the 'default' set of spaces
if space == 'trailing':
return any(s in config['spaces'] for s in [space, 'all'])
else:
return any(s in config['spaces'] for s in [space, 'all', 'default'])
@staticmethod
def _read_results_pickle(results_file: Path) -> List:
"""
Read hyperopt results from pickle file
LEGACY method - new files are written as json and cannot be read with this method.
"""
from joblib import load
logger.info(f"Reading pickled epochs from '{results_file}'")
data = load(results_file)
return data
@staticmethod
def _read_results(results_file: Path) -> List:
"""
Read hyperopt results from file
"""
logger.info("Reading epochs from '%s'", results_file)
data = load(results_file)
import rapidjson
logger.info(f"Reading epochs from '{results_file}'")
with results_file.open('r') as f:
data = [rapidjson.loads(line) for line in f]
return data
@staticmethod
@@ -37,7 +61,10 @@ class HyperoptTools():
"""
epochs: List = []
if results_file.is_file() and results_file.stat().st_size > 0:
epochs = HyperoptTools._read_results(results_file)
if results_file.suffix == '.pickle':
epochs = HyperoptTools._read_results_pickle(results_file)
else:
epochs = HyperoptTools._read_results(results_file)
# Detection of some old format, without 'is_best' field saved
if epochs[0].get('is_best') is None:
raise OperationalException(
@@ -53,6 +80,7 @@ class HyperoptTools():
Display details of the hyperopt result
"""
params = results.get('params_details', {})
non_optimized = results.get('params_not_optimized', {})
# Default header string
if header_str is None:
@@ -69,8 +97,10 @@ class HyperoptTools():
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
else:
HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:")
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:")
HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:",
non_optimized)
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
non_optimized)
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:")
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:")
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:")
@@ -96,12 +126,12 @@ class HyperoptTools():
result_dict.update(space_params)
@staticmethod
def _params_pretty_print(params, space: str, header: str) -> None:
if space in params:
def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None:
if space in params or space in non_optimized:
space_params = HyperoptTools._space_params(params, space, 5)
params_result = f"\n# {header}\n"
result = f"\n# {header}\n"
if space == 'stoploss':
params_result += f"stoploss = {space_params.get('stoploss')}"
result += f"stoploss = {space_params.get('stoploss')}"
elif space == 'roi':
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
@@ -110,28 +140,64 @@ class HyperoptTools():
(str(k), v) for k, v in space_params.items()
),
default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
params_result += f"minimal_roi = {minimal_roi_result}"
result += f"minimal_roi = {minimal_roi_result}"
elif space == 'trailing':
for k, v in space_params.items():
params_result += f'{k} = {v}\n'
result += f'{k} = {v}\n'
else:
params_result += f"{space}_params = {pformat(space_params, indent=4)}"
params_result = params_result.replace("}", "\n}").replace("{", "{\n ")
no_params = HyperoptTools._space_params(non_optimized, space, 5)
params_result = params_result.replace("\n", "\n ")
print(params_result)
result += f"{space}_params = {HyperoptTools._pprint(space_params, no_params)}"
result = result.replace("\n", "\n ")
print(result)
@staticmethod
def _space_params(params, space: str, r: int = None) -> Dict:
d = params[space]
# Round floats to `r` digits after the decimal point if requested
return round_dict(d, r) if r else d
d = params.get(space)
if d:
# Round floats to `r` digits after the decimal point if requested
return round_dict(d, r) if r else d
return {}
@staticmethod
def _pprint(params, non_optimized, indent: int = 4):
"""
Pretty-print hyperopt results (based on 2 dicts - with add. comment)
"""
p = params.copy()
p.update(non_optimized)
result = '{\n'
for k, param in p.items():
result += " " * indent + f'"{k}": '
result += f'"{param}",' if isinstance(param, str) else f'{param},'
if k in non_optimized:
result += " # value loaded from strategy"
result += "\n"
result += '}'
return result
@staticmethod
def is_best_loss(results, current_best_loss: float) -> bool:
return results['loss'] < current_best_loss
return bool(results['loss'] < current_best_loss)
@staticmethod
def format_results_explanation_string(results_metrics: Dict, stake_currency: str) -> str:
"""
Return the formatted results explanation in a string
"""
return (f"{results_metrics['total_trades']:6d} trades. "
f"{results_metrics['wins']}/{results_metrics['draws']}"
f"/{results_metrics['losses']} Wins/Draws/Losses. "
f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. "
f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. "
f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} "
f"({results_metrics['profit_total'] * 100: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). "
f"Avg duration {results_metrics['holding_avg']} min."
).encode(locale.getpreferredencoding(), 'replace').decode('utf-8')
@staticmethod
def _format_explanation_string(results, total_epochs) -> str:
@@ -156,12 +222,27 @@ class HyperoptTools():
if 'results_metrics.winsdrawslosses' not in trials.columns:
# Ensure compatibility with older versions of hyperopt results
trials['results_metrics.winsdrawslosses'] = 'N/A'
legacy_mode = True
if 'results_metrics.total_trades' in trials:
legacy_mode = False
# New mode, using backtest result for metrics
trials['results_metrics.winsdrawslosses'] = trials.apply(
lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} "
f"{x['results_metrics.losses']:>4}", axis=1)
trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades',
'results_metrics.winsdrawslosses',
'results_metrics.profit_mean', 'results_metrics.profit_total_abs',
'results_metrics.profit_total', 'results_metrics.holding_avg',
'loss', 'is_initial_point', 'is_best']]
else:
# Legacy mode
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.winsdrawslosses',
'results_metrics.avg_profit', 'results_metrics.total_profit',
'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']]
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.winsdrawslosses',
'results_metrics.avg_profit', 'results_metrics.total_profit',
'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']]
trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
'Total profit', 'Profit', 'Avg duration', 'Objective',
'is_initial_point', 'is_best']
@@ -171,26 +252,28 @@ class HyperoptTools():
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Trades'] = trials['Trades'].astype(str)
perc_multi = 1 if legacy_mode else 100
trials['Epoch'] = trials['Epoch'].apply(
lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs)
)
trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
lambda x: f'{x * perc_multi:,.2f}%'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
)
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
lambda x: f'{x:,.1f} m'.rjust(7, ' ') if isinstance(x, float) else f"{x}"
if not isna(x) else "--".rjust(7, ' ')
)
trials['Objective'] = trials['Objective'].apply(
lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ')
lambda x: f'{x:,.5f}'.rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ')
)
stake_currency = config['stake_currency']
trials['Profit'] = trials.apply(
lambda x: '{:,.8f} {} {}'.format(
x['Total profit'], config['stake_currency'],
'({:,.2f}%)'.format(x['Profit']).rjust(10, ' ')
).rjust(25+len(config['stake_currency']))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])),
lambda x: '{} {}'.format(
round_coin_value(x['Total profit'], stake_currency),
'({:,.2f}%)'.format(x['Profit'] * perc_multi).rjust(10, ' ')
).rjust(25+len(stake_currency))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)),
axis=1
)
trials = trials.drop(columns=['Total profit'])
@@ -251,11 +334,21 @@ class HyperoptTools():
trials['Best'] = ''
trials['Stake currency'] = config['stake_currency']
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.avg_profit', 'results_metrics.median_profit',
'results_metrics.total_profit',
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']
if 'results_metrics.total_trades' in trials:
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
'results_metrics.profit_mean', 'results_metrics.profit_median',
'results_metrics.profit_total',
'Stake currency',
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
'loss', 'is_initial_point', 'is_best']
perc_multi = 100
else:
perc_multi = 1
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.avg_profit', 'results_metrics.median_profit',
'results_metrics.total_profit',
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
trials = trials[base_metrics + param_metrics]
@@ -272,21 +365,24 @@ class HyperoptTools():
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Epoch'] = trials['Epoch'].astype(str)
trials['Trades'] = trials['Trades'].astype(str)
trials['Median profit'] = trials['Median profit'] * perc_multi
trials['Total profit'] = trials['Total profit'].apply(
lambda x: '{:,.8f}'.format(x) if x != 0.0 else ""
lambda x: f'{x:,.8f}' if x != 0.0 else ""
)
trials['Profit'] = trials['Profit'].apply(
lambda x: '{:,.2f}'.format(x) if not isna(x) else ""
lambda x: f'{x:,.2f}' if not isna(x) else ""
)
trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: '{:,.2f}%'.format(x) if not isna(x) else ""
)
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: '{:,.1f} m'.format(x) if not isna(x) else ""
lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else ""
)
if perc_multi == 1:
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: f'{x:,.1f} m' if isinstance(
x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else ""
)
trials['Objective'] = trials['Objective'].apply(
lambda x: '{:,.5f}'.format(x) if x != 100000 else ""
lambda x: f'{x:,.5f}' if x != 100000 else ""
)
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])

View File

@@ -3,7 +3,6 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Union
from arrow import Arrow
from numpy import int64
from pandas import DataFrame
from tabulate import tabulate
@@ -44,7 +43,7 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]:
Generate floatformat (goes in line with _generate_result_line())
"""
return ['s', 'd', '.2f', '.2f', f'.{decimals_per_coin(stake_currency)}f',
'.2f', 'd', 'd', 'd', 'd']
'.2f', 'd', 's', 's']
def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
@@ -53,7 +52,17 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
"""
return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %',
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
'Wins', 'Draws', 'Losses']
'Win Draw Loss Win%']
def _generate_wins_draws_losses(wins, draws, losses):
if wins > 0 and losses == 0:
wl_ratio = '100'
elif wins == 0:
wl_ratio = '0'
else:
wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100'
return f'{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}'
def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict:
@@ -153,7 +162,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
return tabular_data
def generate_strategy_metrics(all_results: Dict) -> List[Dict]:
def generate_strategy_comparison(all_results: Dict) -> List[Dict]:
"""
Generate summary per strategy
:param all_results: Dict of <Strategyname: DataFrame> containing results for all strategies
@@ -165,6 +174,17 @@ def generate_strategy_metrics(all_results: Dict) -> List[Dict]:
tabular_data.append(_generate_result_line(
results['results'], results['config']['dry_run_wallet'], strategy)
)
try:
max_drawdown_per, _, _, _, _ = calculate_max_drawdown(results['results'],
value_col='profit_ratio')
max_drawdown_abs, _, _, _, _ = calculate_max_drawdown(results['results'],
value_col='profit_abs')
except ValueError:
max_drawdown_per = 0
max_drawdown_abs = 0
tabular_data[-1]['max_drawdown_per'] = round(max_drawdown_per * 100, 2)
tabular_data[-1]['max_drawdown_abs'] = \
round_coin_value(max_drawdown_abs, results['config']['stake_currency'], False)
return tabular_data
@@ -194,7 +214,40 @@ def generate_edge_table(results: dict) -> str:
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
""" Generate overall trade statistics """
if len(results) == 0:
return {
'wins': 0,
'losses': 0,
'draws': 0,
'holding_avg': timedelta(),
'winner_holding_avg': timedelta(),
'loser_holding_avg': timedelta(),
}
winning_trades = results.loc[results['profit_ratio'] > 0]
draw_trades = results.loc[results['profit_ratio'] == 0]
losing_trades = results.loc[results['profit_ratio'] < 0]
zero_duration_trades = len(results.loc[(results['trade_duration'] == 0) &
(results['sell_reason'] == 'trailing_stop_loss')])
return {
'wins': len(winning_trades),
'losses': len(losing_trades),
'draws': len(draw_trades),
'holding_avg': (timedelta(minutes=round(results['trade_duration'].mean()))
if not results.empty else timedelta()),
'winner_holding_avg': (timedelta(minutes=round(winning_trades['trade_duration'].mean()))
if not winning_trades.empty else timedelta()),
'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean()))
if not losing_trades.empty else timedelta()),
'zero_duration_trades': zero_duration_trades,
}
def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
""" Generate daily statistics """
if len(results) == 0:
return {
'backtest_best_day': 0,
@@ -204,8 +257,6 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
'winning_days': 0,
'draw_days': 0,
'losing_days': 0,
'winner_holding_avg': timedelta(),
'loser_holding_avg': timedelta(),
}
daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum()
daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10)
@@ -217,9 +268,6 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
draw_days = sum(daily_profit == 0)
losing_days = sum(daily_profit < 0)
winning_trades = results.loc[results['profit_ratio'] > 0]
losing_trades = results.loc[results['profit_ratio'] < 0]
return {
'backtest_best_day': best_rel,
'backtest_worst_day': worst_rel,
@@ -228,16 +276,152 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
'winning_days': winning_days,
'draw_days': draw_days,
'losing_days': losing_days,
'winner_holding_avg': (timedelta(minutes=round(winning_trades['trade_duration'].mean()))
if not winning_trades.empty else timedelta()),
'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean()))
if not losing_trades.empty else timedelta()),
}
def generate_strategy_stats(btdata: Dict[str, DataFrame],
strategy: str,
content: Dict[str, Any],
min_date: datetime, max_date: datetime,
market_change: float
) -> Dict[str, Any]:
"""
:param btdata: Backtest data
:param strategy: Strategy name
:param content: Backtest result data in the format:
{'results: results, 'config: config}}.
:param min_date: Backtest start date
:param max_date: Backtest end date
:param market_change: float indicating the market change
:return: Dictionary containing results per strategy and a stratgy summary.
"""
results: Dict[str, DataFrame] = content['results']
if not isinstance(results, DataFrame):
return {}
config = content['config']
max_open_trades = min(config['max_open_trades'], len(btdata.keys()))
starting_balance = config['dry_run_wallet']
stake_currency = config['stake_currency']
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
starting_balance=starting_balance,
results=results, skip_nan=False)
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
results=results)
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
starting_balance=starting_balance,
results=results.loc[results['is_open']],
skip_nan=True)
daily_stats = generate_daily_stats(results)
trade_stats = generate_trading_stats(results)
best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
results['open_timestamp'] = results['open_date'].astype(int64) // 1e6
results['close_timestamp'] = results['close_date'].astype(int64) // 1e6
backtest_days = (max_date - min_date).days
strat_stats = {
'trades': results.to_dict(orient='records'),
'locks': [lock.to_json() for lock in content['locks']],
'best_pair': best_pair,
'worst_pair': worst_pair,
'results_per_pair': pair_results,
'sell_reason_summary': sell_reason_stats,
'left_open_trades': left_open_results,
'total_trades': len(results),
'total_volume': float(results['stake_amount'].sum()),
'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0,
'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0,
'profit_total': results['profit_abs'].sum() / starting_balance,
'profit_total_abs': results['profit_abs'].sum(),
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
'backtest_start_ts': int(min_date.timestamp() * 1000),
'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
'backtest_end_ts': int(max_date.timestamp() * 1000),
'backtest_days': backtest_days,
'backtest_run_start_ts': content['backtest_start_time'],
'backtest_run_end_ts': content['backtest_end_time'],
'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
'market_change': market_change,
'pairlist': list(btdata.keys()),
'stake_amount': config['stake_amount'],
'stake_currency': config['stake_currency'],
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
'starting_balance': starting_balance,
'dry_run_wallet': starting_balance,
'final_balance': content['final_balance'],
'rejected_signals': content['rejected_signals'],
'max_open_trades': max_open_trades,
'max_open_trades_setting': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1),
'timeframe': config['timeframe'],
'timerange': config.get('timerange', ''),
'enable_protections': config.get('enable_protections', False),
'strategy_name': strategy,
# Parameters relevant for backtesting
'stoploss': config['stoploss'],
'trailing_stop': config.get('trailing_stop', False),
'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0),
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False),
'use_custom_stoploss': config.get('use_custom_stoploss', False),
'minimal_roi': config['minimal_roi'],
'use_sell_signal': config['ask_strategy']['use_sell_signal'],
'sell_profit_only': config['ask_strategy']['sell_profit_only'],
'sell_profit_offset': config['ask_strategy']['sell_profit_offset'],
'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'],
**daily_stats,
**trade_stats
}
try:
max_drawdown, _, _, _, _ = calculate_max_drawdown(
results, value_col='profit_ratio')
drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown(
results, value_col='profit_abs')
strat_stats.update({
'max_drawdown': max_drawdown,
'max_drawdown_abs': drawdown_abs,
'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT),
'drawdown_start_ts': drawdown_start.timestamp() * 1000,
'drawdown_end': drawdown_end.strftime(DATETIME_PRINT_FORMAT),
'drawdown_end_ts': drawdown_end.timestamp() * 1000,
'max_drawdown_low': low_val,
'max_drawdown_high': high_val,
})
csum_min, csum_max = calculate_csum(results, starting_balance)
strat_stats.update({
'csum_min': csum_min,
'csum_max': csum_max
})
except ValueError:
strat_stats.update({
'max_drawdown': 0.0,
'max_drawdown_abs': 0.0,
'max_drawdown_low': 0.0,
'max_drawdown_high': 0.0,
'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc),
'drawdown_start_ts': 0,
'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc),
'drawdown_end_ts': 0,
'csum_min': 0,
'csum_max': 0
})
return strat_stats
def generate_backtest_stats(btdata: Dict[str, DataFrame],
all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]],
min_date: Arrow, max_date: Arrow
min_date: datetime, max_date: datetime
) -> Dict[str, Any]:
"""
:param btdata: Backtest data
@@ -245,132 +429,17 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
{ Strategy: {'results: results, 'config: config}}.
:param min_date: Backtest start date
:param max_date: Backtest end date
:return:
Dictionary containing results per strategy and a stratgy summary.
:return: Dictionary containing results per strategy and a stratgy summary.
"""
result: Dict[str, Any] = {'strategy': {}}
market_change = calculate_market_change(btdata, 'close')
for strategy, content in all_results.items():
results: Dict[str, DataFrame] = content['results']
if not isinstance(results, DataFrame):
continue
config = content['config']
max_open_trades = min(config['max_open_trades'], len(btdata.keys()))
starting_balance = config['dry_run_wallet']
stake_currency = config['stake_currency']
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
starting_balance=starting_balance,
results=results, skip_nan=False)
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
results=results)
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
starting_balance=starting_balance,
results=results.loc[results['is_open']],
skip_nan=True)
daily_stats = generate_daily_stats(results)
best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
results['open_timestamp'] = results['open_date'].astype(int64) // 1e6
results['close_timestamp'] = results['close_date'].astype(int64) // 1e6
backtest_days = (max_date - min_date).days
strat_stats = {
'trades': results.to_dict(orient='records'),
'locks': [lock.to_json() for lock in content['locks']],
'best_pair': best_pair,
'worst_pair': worst_pair,
'results_per_pair': pair_results,
'sell_reason_summary': sell_reason_stats,
'left_open_trades': left_open_results,
'total_trades': len(results),
'total_volume': float(results['stake_amount'].sum()),
'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0,
'profit_total': results['profit_abs'].sum() / starting_balance,
'profit_total_abs': results['profit_abs'].sum(),
'backtest_start': min_date.datetime,
'backtest_start_ts': min_date.int_timestamp * 1000,
'backtest_end': max_date.datetime,
'backtest_end_ts': max_date.int_timestamp * 1000,
'backtest_days': backtest_days,
'backtest_run_start_ts': content['backtest_start_time'],
'backtest_run_end_ts': content['backtest_end_time'],
'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
'market_change': market_change,
'pairlist': list(btdata.keys()),
'stake_amount': config['stake_amount'],
'stake_currency': config['stake_currency'],
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
'starting_balance': starting_balance,
'dry_run_wallet': starting_balance,
'final_balance': content['final_balance'],
'max_open_trades': max_open_trades,
'max_open_trades_setting': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1),
'timeframe': config['timeframe'],
'timerange': config.get('timerange', ''),
'enable_protections': config.get('enable_protections', False),
'strategy_name': strategy,
# Parameters relevant for backtesting
'stoploss': config['stoploss'],
'trailing_stop': config.get('trailing_stop', False),
'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0),
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False),
'use_custom_stoploss': config.get('use_custom_stoploss', False),
'minimal_roi': config['minimal_roi'],
'use_sell_signal': config['ask_strategy']['use_sell_signal'],
'sell_profit_only': config['ask_strategy']['sell_profit_only'],
'sell_profit_offset': config['ask_strategy']['sell_profit_offset'],
'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'],
**daily_stats,
}
strat_stats = generate_strategy_stats(btdata, strategy, content,
min_date, max_date, market_change=market_change)
result['strategy'][strategy] = strat_stats
try:
max_drawdown, _, _, _, _ = calculate_max_drawdown(
results, value_col='profit_ratio')
drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown(
results, value_col='profit_abs')
strat_stats.update({
'max_drawdown': max_drawdown,
'max_drawdown_abs': drawdown_abs,
'drawdown_start': drawdown_start,
'drawdown_start_ts': drawdown_start.timestamp() * 1000,
'drawdown_end': drawdown_end,
'drawdown_end_ts': drawdown_end.timestamp() * 1000,
'max_drawdown_low': low_val,
'max_drawdown_high': high_val,
})
csum_min, csum_max = calculate_csum(results, starting_balance)
strat_stats.update({
'csum_min': csum_min,
'csum_max': csum_max
})
except ValueError:
strat_stats.update({
'max_drawdown': 0.0,
'max_drawdown_abs': 0.0,
'max_drawdown_low': 0.0,
'max_drawdown_high': 0.0,
'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc),
'drawdown_start_ts': 0,
'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc),
'drawdown_end_ts': 0,
'csum_min': 0,
'csum_max': 0
})
strategy_results = generate_strategy_metrics(all_results=all_results)
strategy_results = generate_strategy_comparison(all_results=all_results)
result['strategy_comparison'] = strategy_results
@@ -393,7 +462,8 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
floatfmt = _get_line_floatfmt(stake_currency)
output = [[
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
t['profit_total_pct'], t['duration_avg'],
_generate_wins_draws_losses(t['wins'], t['draws'], t['losses'])
] for t in pair_results]
# Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(output, headers=headers,
@@ -410,9 +480,7 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
headers = [
'Sell Reason',
'Sells',
'Wins',
'Draws',
'Losses',
'Win Draws Loss Win%',
'Avg Profit %',
'Cum Profit %',
f'Tot Profit {stake_currency}',
@@ -420,7 +488,8 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
]
output = [[
t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'],
t['sell_reason'], t['trades'],
_generate_wins_draws_losses(t['wins'], t['draws'], t['losses']),
t['profit_mean_pct'], t['profit_sum_pct'],
round_coin_value(t['profit_total_abs'], stake_currency, False),
t['profit_total_pct'],
@@ -438,11 +507,22 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str:
"""
floatfmt = _get_line_floatfmt(stake_currency)
headers = _get_line_header('Strategy', stake_currency)
# _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless
# therefore we slip this column in only for strategy summary here.
headers.append('Drawdown')
# Align drawdown string on the center two space separator.
drawdown = [f'{t["max_drawdown_per"]:.2f}' for t in strategy_results]
dd_pad_abs = max([len(t['max_drawdown_abs']) for t in strategy_results])
dd_pad_per = max([len(dd) for dd in drawdown])
drawdown = [f'{t["max_drawdown_abs"]:>{dd_pad_abs}} {stake_currency} {dd:>{dd_pad_per}}%'
for t, dd in zip(strategy_results, drawdown)]
output = [[
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
] for t in strategy_results]
t['profit_total_pct'], t['duration_avg'],
_generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown]
for t, drawdown in zip(strategy_results, drawdown)]
# Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(output, headers=headers,
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
@@ -452,9 +532,21 @@ def text_table_add_metrics(strat_results: Dict) -> str:
if len(strat_results['trades']) > 0:
best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio'])
worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio'])
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
# command stores these results and newer version of freqtrade must be able to handle old
# results with missing new fields.
zero_duration_trades = '--'
if 'zero_duration_trades' in strat_results:
zero_duration_trades_per = \
100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades']
zero_duration_trades = f'{zero_duration_trades_per:.2f}% ' \
f'({strat_results["zero_duration_trades"]})'
metrics = [
('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)),
('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)),
('Backtesting from', strat_results['backtest_start']),
('Backtesting to', strat_results['backtest_end']),
('Max open trades', strat_results['max_open_trades']),
('', ''), # Empty line to improve readability
('Total trades', strat_results['total_trades']),
@@ -464,13 +556,12 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])),
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
strat_results['stake_currency'])),
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"),
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"),
('Trades per day', strat_results['trades_per_day']),
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
strat_results['stake_currency'])),
('Total trade volume', round_coin_value(strat_results['total_volume'],
strat_results['stake_currency'])),
('', ''), # Empty line to improve readability
('Best Pair', f"{strat_results['best_pair']['key']} "
f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"),
@@ -488,6 +579,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
('Zero Duration Trades', zero_duration_trades),
('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')),
('', ''), # Empty line to improve readability
('Min balance', round_coin_value(strat_results['csum_min'],
@@ -502,8 +595,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])),
('Drawdown low', round_coin_value(strat_results['max_drawdown_low'],
strat_results['stake_currency'])),
('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)),
('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)),
('Drawdown Start', strat_results['drawdown_start']),
('Drawdown End', strat_results['drawdown_end']),
('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"),
]
@@ -522,37 +615,43 @@ def text_table_add_metrics(strat_results: Dict) -> str:
return message
def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str):
"""
Print results for one strategy
"""
# Print results
print(f"Result for strategy {strategy}")
table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency)
if isinstance(table, str):
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
stake_currency=stake_currency)
if isinstance(table, str) and len(table) > 0:
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
print(table)
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
if isinstance(table, str) and len(table) > 0:
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
table = text_table_add_metrics(results)
if isinstance(table, str) and len(table) > 0:
print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '='))
print(table)
if isinstance(table, str) and len(table) > 0:
print('=' * len(table.splitlines()[0]))
print()
def show_backtest_results(config: Dict, backtest_stats: Dict):
stake_currency = config['stake_currency']
for strategy, results in backtest_stats['strategy'].items():
# Print results
print(f"Result for strategy {strategy}")
table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency)
if isinstance(table, str):
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
stake_currency=stake_currency)
if isinstance(table, str) and len(table) > 0:
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
print(table)
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
if isinstance(table, str) and len(table) > 0:
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
table = text_table_add_metrics(results)
if isinstance(table, str) and len(table) > 0:
print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '='))
print(table)
if isinstance(table, str) and len(table) > 0:
print('=' * len(table.splitlines()[0]))
print()
show_backtest_result(strategy, results, stake_currency)
if len(backtest_stats['strategy']) > 1:
# Print Strategy summary table

View File

@@ -123,6 +123,27 @@ def migrate_open_orders_to_trades(engine):
""")
def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, cols: List):
# Schema migration necessary
engine.execute(f"alter table orders rename to {table_back_name}")
# drop indexes on backup table
for index in inspector.get_indexes(table_back_name):
engine.execute(f"drop index {index['name']}")
# let SQLAlchemy create the schema as required
decl_base.metadata.create_all(engine)
engine.execute(f"""
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status,
symbol, order_type, side, price, amount, filled, average, remaining, cost, order_date,
order_filled_date, order_update_date)
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status,
symbol, order_type, side, price, amount, filled, null average, remaining, cost, order_date,
order_filled_date, order_update_date
from {table_back_name}
""")
def check_migrate(engine, decl_base, previous_tables) -> None:
"""
Checks if migration is necessary and migrates if necessary
@@ -145,6 +166,11 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
logger.info('Moving open orders to Orders table.')
migrate_open_orders_to_trades(engine)
else:
pass
# Empty for now - as there is only one iteration of the orders table so far.
# table_back_name = get_backup_name(tabs, 'orders_bak')
cols_order = inspector.get_columns('orders')
if not has_column(cols_order, 'average'):
tabs = get_table_names_for_table(inspector, 'orders')
# Empty for now - as there is only one iteration of the orders table so far.
table_back_name = get_backup_name(tabs, 'orders_bak')
migrate_orders_table(decl_base, inspector, engine, table_back_name, cols)

View File

@@ -112,16 +112,17 @@ class Order(_DECL_BASE):
trade = relationship("Trade", back_populates="orders")
ft_order_side = Column(String, nullable=False)
ft_pair = Column(String, nullable=False)
ft_order_side = Column(String(25), nullable=False)
ft_pair = Column(String(25), nullable=False)
ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
order_id = Column(String, nullable=False, index=True)
status = Column(String, nullable=True)
symbol = Column(String, nullable=True)
order_type = Column(String, nullable=True)
side = Column(String, nullable=True)
order_id = Column(String(255), nullable=False, index=True)
status = Column(String(255), nullable=True)
symbol = Column(String(25), nullable=True)
order_type = Column(String(50), nullable=True)
side = Column(String(25), nullable=True)
price = Column(Float, nullable=True)
average = Column(Float, nullable=True)
amount = Column(Float, nullable=True)
filled = Column(Float, nullable=True)
remaining = Column(Float, nullable=True)
@@ -150,6 +151,7 @@ class Order(_DECL_BASE):
self.price = order.get('price', self.price)
self.amount = order.get('amount', self.amount)
self.filled = order.get('filled', self.filled)
self.average = order.get('average', self.average)
self.remaining = order.get('remaining', self.remaining)
self.cost = order.get('cost', self.cost)
if 'timestamp' in order and order['timestamp'] is not None:
@@ -567,23 +569,6 @@ class LocalTrade():
else:
return None
@staticmethod
def get_trades(trade_filter=None) -> Query:
"""
Helper function to query Trades using filters.
:param trade_filter: Optional filter to apply to trades
Can be either a Filter object, or a List of filters
e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])`
e.g. `(trade_filter=Trade.id == trade_id)`
:return: unsorted query object
"""
if trade_filter is not None:
if not isinstance(trade_filter, list):
trade_filter = [trade_filter]
return Trade.query.filter(*trade_filter)
else:
return Trade.query
@staticmethod
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
open_date: datetime = None, close_date: datetime = None,
@@ -636,83 +621,7 @@ class LocalTrade():
"""
Query trades from persistence layer
"""
return Trade.get_trades(Trade.is_open.is_(True)).all()
@staticmethod
def get_open_order_trades():
"""
Returns all open trades
"""
return Trade.get_trades(Trade.open_order_id.isnot(None)).all()
@staticmethod
def get_open_trades_without_assigned_fees():
"""
Returns all open trades which don't have open fees set correctly
"""
return Trade.get_trades([Trade.fee_open_currency.is_(None),
Trade.orders.any(),
Trade.is_open.is_(True),
]).all()
@staticmethod
def get_sold_trades_without_assigned_fees():
"""
Returns all closed trades which don't have fees set correctly
"""
return Trade.get_trades([Trade.fee_close_currency.is_(None),
Trade.orders.any(),
Trade.is_open.is_(False),
]).all()
@staticmethod
def total_open_trades_stakes() -> float:
"""
Calculates total invested amount in open trades
in stake currency
"""
if Trade.use_db:
total_open_stake_amount = Trade.query.with_entities(
func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar()
else:
total_open_stake_amount = sum(
t.stake_amount for t in Trade.get_trades_proxy(is_open=True))
return total_open_stake_amount or 0
@staticmethod
def get_overall_performance() -> List[Dict[str, Any]]:
"""
Returns List of dicts containing all Trades, including profit and trade count
"""
pair_rates = Trade.query.with_entities(
Trade.pair,
func.sum(Trade.close_profit).label('profit_sum'),
func.count(Trade.pair).label('count')
).filter(Trade.is_open.is_(False))\
.group_by(Trade.pair) \
.order_by(desc('profit_sum')) \
.all()
return [
{
'pair': pair,
'profit': rate,
'count': count
}
for pair, rate, count in pair_rates
]
@staticmethod
def get_best_pair():
"""
Get best pair with closed trade.
:returns: Tuple containing (pair, profit_sum)
"""
best_pair = Trade.query.with_entities(
Trade.pair, func.sum(Trade.close_profit).label('profit_sum')
).filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(desc('profit_sum')).first()
return best_pair
return Trade.get_trades_proxy(is_open=True)
@staticmethod
def stoploss_reinitialization(desired_stoploss):
@@ -749,15 +658,15 @@ class Trade(_DECL_BASE, LocalTrade):
orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan")
exchange = Column(String, nullable=False)
pair = Column(String, nullable=False, index=True)
exchange = Column(String(25), nullable=False)
pair = Column(String(25), nullable=False, index=True)
is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0)
fee_open_cost = Column(Float, nullable=True)
fee_open_currency = Column(String, nullable=True)
fee_open_currency = Column(String(25), nullable=True)
fee_close = Column(Float, nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True)
fee_close_currency = Column(String, nullable=True)
fee_close_currency = Column(String(25), nullable=True)
open_rate = Column(Float)
open_rate_requested = Column(Float)
# open_trade_value - calculated via _calc_open_trade_value
@@ -771,7 +680,7 @@ class Trade(_DECL_BASE, LocalTrade):
amount_requested = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime)
open_order_id = Column(String)
open_order_id = Column(String(255))
# absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0)
# percentage value of the stop loss
@@ -781,16 +690,16 @@ class Trade(_DECL_BASE, LocalTrade):
# percentage value of the initial stop loss
initial_stop_loss_pct = Column(Float, nullable=True)
# stoploss order id which is on exchange
stoploss_order_id = Column(String, nullable=True, index=True)
stoploss_order_id = Column(String(255), nullable=True, index=True)
# last update time of the stoploss order on exchange
stoploss_last_update = Column(DateTime, nullable=True)
# absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0)
# Lowest price reached
min_rate = Column(Float, nullable=True)
sell_reason = Column(String, nullable=True)
sell_order_status = Column(String, nullable=True)
strategy = Column(String, nullable=True)
sell_reason = Column(String(100), nullable=True)
sell_order_status = Column(String(100), nullable=True)
strategy = Column(String(100), nullable=True)
timeframe = Column(Integer, nullable=True)
def __init__(self, **kwargs):
@@ -810,7 +719,7 @@ class Trade(_DECL_BASE, LocalTrade):
open_date: datetime = None, close_date: datetime = None,
) -> List['LocalTrade']:
"""
Helper function to query Trades.
Helper function to query Trades.j
Returns a List of trades, filtered on the parameters given.
In live mode, converts the filter to a database query and returns all rows
In Backtest mode, uses filters on Trade.trades to get the result.
@@ -835,6 +744,109 @@ class Trade(_DECL_BASE, LocalTrade):
close_date=close_date
)
@staticmethod
def get_trades(trade_filter=None) -> Query:
"""
Helper function to query Trades using filters.
NOTE: Not supported in Backtesting.
:param trade_filter: Optional filter to apply to trades
Can be either a Filter object, or a List of filters
e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])`
e.g. `(trade_filter=Trade.id == trade_id)`
:return: unsorted query object
"""
if not Trade.use_db:
raise NotImplementedError('`Trade.get_trades()` not supported in backtesting mode.')
if trade_filter is not None:
if not isinstance(trade_filter, list):
trade_filter = [trade_filter]
return Trade.query.filter(*trade_filter)
else:
return Trade.query
@staticmethod
def get_open_order_trades():
"""
Returns all open trades
NOTE: Not supported in Backtesting.
"""
return Trade.get_trades(Trade.open_order_id.isnot(None)).all()
@staticmethod
def get_open_trades_without_assigned_fees():
"""
Returns all open trades which don't have open fees set correctly
NOTE: Not supported in Backtesting.
"""
return Trade.get_trades([Trade.fee_open_currency.is_(None),
Trade.orders.any(),
Trade.is_open.is_(True),
]).all()
@staticmethod
def get_sold_trades_without_assigned_fees():
"""
Returns all closed trades which don't have fees set correctly
NOTE: Not supported in Backtesting.
"""
return Trade.get_trades([Trade.fee_close_currency.is_(None),
Trade.orders.any(),
Trade.is_open.is_(False),
]).all()
@staticmethod
def total_open_trades_stakes() -> float:
"""
Calculates total invested amount in open trades
in stake currency
"""
if Trade.use_db:
total_open_stake_amount = Trade.query.with_entities(
func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar()
else:
total_open_stake_amount = sum(
t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True))
return total_open_stake_amount or 0
@staticmethod
def get_overall_performance() -> List[Dict[str, Any]]:
"""
Returns List of dicts containing all Trades, including profit and trade count
NOTE: Not supported in Backtesting.
"""
pair_rates = Trade.query.with_entities(
Trade.pair,
func.sum(Trade.close_profit).label('profit_sum'),
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
func.count(Trade.pair).label('count')
).filter(Trade.is_open.is_(False))\
.group_by(Trade.pair) \
.order_by(desc('profit_sum_abs')) \
.all()
return [
{
'pair': pair,
'profit': profit,
'profit_abs': profit_abs,
'count': count
}
for pair, profit, profit_abs, count in pair_rates
]
@staticmethod
def get_best_pair():
"""
Get best pair with closed trade.
NOTE: Not supported in Backtesting.
:returns: Tuple containing (pair, profit_sum)
"""
best_pair = Trade.query.with_entities(
Trade.pair, func.sum(Trade.close_profit).label('profit_sum')
).filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(desc('profit_sum')).first()
return best_pair
class PairLock(_DECL_BASE):
"""
@@ -844,8 +856,8 @@ class PairLock(_DECL_BASE):
id = Column(Integer, primary_key=True)
pair = Column(String, nullable=False, index=True)
reason = Column(String, nullable=True)
pair = Column(String(25), nullable=False, index=True)
reason = Column(String(255), nullable=True)
# Time the pair was locked (start time)
lock_time = Column(DateTime, nullable=False)
# Time until the pair is locked (end time)

View File

@@ -77,7 +77,8 @@ def init_plotscript(config, markets: List, startup_candles: int = 0):
)
except ValueError as e:
raise OperationalException(e) from e
trades = trim_dataframe(trades, timerange, 'open_date')
if not trades.empty:
trades = trim_dataframe(trades, timerange, 'open_date')
return {"ohlcv": data,
"trades": trades,
@@ -540,8 +541,11 @@ def load_and_plot_trades(config: Dict[str, Any]):
df_analyzed = strategy.analyze_ticker(data, {'pair': pair})
df_analyzed = trim_dataframe(df_analyzed, timerange)
trades_pair = trades.loc[trades['pair'] == pair]
trades_pair = extract_trades_of_period(df_analyzed, trades_pair)
if not trades.empty:
trades_pair = trades.loc[trades['pair'] == pair]
trades_pair = extract_trades_of_period(df_analyzed, trades_pair)
else:
trades_pair = trades
fig = generate_candlestick_graph(
pair=pair,

View File

@@ -71,14 +71,14 @@ class AgeFilter(IPairList):
daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None
if not self._validate_pair_loc(p, daily_candles):
pairlist.remove(p)
logger.info(f"Validated {len(pairlist)} pairs.")
self.log_once(f"Validated {len(pairlist)} pairs.", logger.info)
return pairlist
def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool:
"""
Validate age for the ticker
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.load_markets()
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
:return: True if the pair can stay, false if it should be removed
"""
# Check symbol in cache
@@ -86,7 +86,7 @@ class AgeFilter(IPairList):
return True
if daily_candles is not None:
if len(daily_candles) > self._min_days_listed:
if len(daily_candles) >= self._min_days_listed:
# We have fetched at least the minimum required number of daily candles
# Add to cache, store the time we last checked this symbol
self._symbolsChecked[pair] = int(arrow.utcnow().float_timestamp) * 1000

View File

@@ -7,7 +7,7 @@ from copy import deepcopy
from typing import Any, Dict, List
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import market_is_active
from freqtrade.exchange import Exchange, market_is_active
from freqtrade.mixins import LoggingMixin
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
class IPairList(LoggingMixin, ABC):
def __init__(self, exchange, pairlistmanager,
def __init__(self, exchange: Exchange, pairlistmanager,
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
pairlist_pos: int) -> None:
"""
@@ -28,7 +28,7 @@ class IPairList(LoggingMixin, ABC):
"""
self._enabled = True
self._exchange = exchange
self._exchange: Exchange = exchange
self._pairlistmanager = pairlistmanager
self._config = config
self._pairlistconfig = pairlistconfig
@@ -68,7 +68,7 @@ class IPairList(LoggingMixin, ABC):
filter_pairlist() method.
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.load_markets()
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
:return: True if the pair can stay, false if it should be removed
"""
raise NotImplementedError()

View File

@@ -39,7 +39,12 @@ class PerformanceFilter(IPairList):
:return: new allowlist
"""
# Get the trading performance for pairs from database
performance = pd.DataFrame(Trade.get_overall_performance())
try:
performance = pd.DataFrame(Trade.get_overall_performance())
except AttributeError:
# Performancefilter does not work in backtesting.
self.log_once("PerformanceFilter is not available in this mode.", logger.warning)
return pairlist
# Skip performance-based sorting if no performance data is available
if len(performance) == 0:

View File

@@ -48,7 +48,7 @@ class PrecisionFilter(IPairList):
Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very
low value pairs.
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.load_markets()
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
:return: True if the pair can stay, false if it should be removed
"""
stop_price = ticker['ask'] * self._stoploss

View File

@@ -27,9 +27,13 @@ class PriceFilter(IPairList):
self._max_price = pairlistconfig.get('max_price', 0)
if self._max_price < 0:
raise OperationalException("PriceFilter requires max_price to be >= 0")
self._max_value = pairlistconfig.get('max_value', 0)
if self._max_value < 0:
raise OperationalException("PriceFilter requires max_value to be >= 0")
self._enabled = ((self._low_price_ratio > 0) or
(self._min_price > 0) or
(self._max_price > 0))
(self._max_price > 0) or
(self._max_value > 0))
@property
def needstickers(self) -> bool:
@@ -51,6 +55,8 @@ class PriceFilter(IPairList):
active_price_filters.append(f"below {self._min_price:.8f}")
if self._max_price != 0:
active_price_filters.append(f"above {self._max_price:.8f}")
if self._max_value != 0:
active_price_filters.append(f"Value above {self._max_value:.8f}")
if len(active_price_filters):
return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}."
@@ -61,7 +67,7 @@ class PriceFilter(IPairList):
"""
Check if if one price-step (pip) is > than a certain barrier.
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.load_markets()
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
:return: True if the pair can stay, false if it should be removed
"""
if ticker.get('last', None) is None or ticker.get('last') == 0:
@@ -79,6 +85,32 @@ class PriceFilter(IPairList):
f"because 1 unit is {changeperc * 100:.3f}%", logger.info)
return False
# Perform low_amount check
if self._max_value != 0:
price = ticker['last']
market = self._exchange.markets[pair]
limits = market['limits']
if ('amount' in limits and 'min' in limits['amount']
and limits['amount']['min'] is not None):
min_amount = limits['amount']['min']
min_precision = market['precision']['amount']
min_value = min_amount * price
if self._exchange.precisionMode == 4:
# tick size
next_value = (min_amount + min_precision) * price
else:
# Decimal places
min_precision = pow(0.1, min_precision)
next_value = (min_amount + min_precision) * price
diff = next_value - min_value
if diff > self._max_value:
self.log_once(f"Removed {pair} from whitelist, "
f"because min value change of {diff} > {self._max_value}.",
logger.info)
return False
# Perform min_price check.
if self._min_price != 0:
if ticker['last'] < self._min_price:
@@ -89,7 +121,7 @@ class PriceFilter(IPairList):
# Perform max_price check.
if self._max_price != 0:
if ticker['last'] > self._max_price:
self.log_once(f"Removed {ticker['symbol']} from whitelist, "
self.log_once(f"Removed {pair} from whitelist, "
f"because last price > {self._max_price:.8f}", logger.info)
return False

View File

@@ -40,7 +40,7 @@ class SpreadFilter(IPairList):
"""
Validate spread for the ticker
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.load_markets()
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
:return: True if the pair can stay, false if it should be removed
"""
if 'bid' in ticker and 'ask' in ticker and ticker['ask']:

View File

@@ -90,7 +90,7 @@ class VolatilityFilter(IPairList):
"""
Validate trading range
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.load_markets()
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
:return: True if the pair can stay, false if it should be removed
"""
# Check symbol in cache

View File

@@ -83,7 +83,7 @@ class RangeStabilityFilter(IPairList):
"""
Validate trading range
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.load_markets()
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
:return: True if the pair can stay, false if it should be removed
"""
# Check symbol in cache

View File

@@ -57,6 +57,7 @@ class Count(BaseModel):
class PerformanceEntry(BaseModel):
pair: str
profit: float
profit_abs: float
count: int
@@ -268,7 +269,7 @@ class DeleteTrade(BaseModel):
class PlotConfig_(BaseModel):
main_plot: Dict[str, Any]
subplots: Optional[Dict[str, Any]]
subplots: Dict[str, Any]
class PlotConfig(BaseModel):

View File

@@ -3,11 +3,13 @@ Module that define classes to convert Crypto-currency to FIAT
e.g BTC to USD
"""
import datetime
import logging
from typing import Dict
from cachetools.ttl import TTLCache
from pycoingecko import CoinGeckoAPI
from requests.exceptions import RequestException
from freqtrade.constants import SUPPORTED_FIAT
@@ -25,6 +27,7 @@ class CryptoToFiatConverter:
_coingekko: CoinGeckoAPI = None
_cryptomap: Dict = {}
_backoff: float = 0.0
def __new__(cls):
"""
@@ -47,8 +50,21 @@ class CryptoToFiatConverter:
def _load_cryptomap(self) -> None:
try:
coinlistings = self._coingekko.get_coins_list()
# Create mapping table from synbol to coingekko_id
# Create mapping table from symbol to coingekko_id
self._cryptomap = {x['symbol']: x['id'] for x in coinlistings}
except RequestException as request_exception:
if "429" in str(request_exception):
logger.warning(
"Too many requests for Coingecko API, backing off and trying again later.")
# Set backoff timestamp to 60 seconds in the future
self._backoff = datetime.datetime.now().timestamp() + 60
return
# If the request is not a 429 error we want to raise the normal error
logger.error(
"Could not load FIAT Cryptocurrency map for the following problem: {}".format(
request_exception
)
)
except (Exception) as exception:
logger.error(
f"Could not load FIAT Cryptocurrency map for the following problem: {exception}")
@@ -127,6 +143,15 @@ class CryptoToFiatConverter:
if crypto_symbol == fiat_symbol:
return 1.0
if self._cryptomap == {}:
if self._backoff <= datetime.datetime.now().timestamp():
self._load_cryptomap()
# return 0.0 if we still dont have data to check, no reason to proceed
if self._cryptomap == {}:
return 0.0
else:
return 0.0
if crypto_symbol not in self._cryptomap:
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)

View File

@@ -24,7 +24,7 @@ from freqtrade.persistence.models import PairLock
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State
from freqtrade.strategy.interface import SellType
from freqtrade.strategy.interface import SellCheckTuple, SellType
logger = logging.getLogger(__name__)
@@ -178,7 +178,7 @@ class RPC:
current_rate = trade.close_rate
current_profit = trade.calc_profit_ratio(current_rate)
current_profit_abs = trade.calc_profit(current_rate)
current_profit_fiat: Optional[float] = None
# Calculate fiat profit
if self._fiat_converter:
current_profit_fiat = self._fiat_converter.convert_amount(
@@ -220,12 +220,13 @@ class RPC:
return results
def _rpc_status_table(self, stake_currency: str,
fiat_display_currency: str) -> Tuple[List, List]:
fiat_display_currency: str) -> Tuple[List, List, float]:
trades = Trade.get_open_trades()
if not trades:
raise RPCException('no active trade')
else:
trades_list = []
fiat_profit_sum = NAN
for trade in trades:
# calculate profit and send message to user
try:
@@ -243,6 +244,8 @@ class RPC:
)
if fiat_profit and not isnan(fiat_profit):
profit_str += f" ({fiat_profit:.2f})"
fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \
else fiat_profit_sum + fiat_profit
trades_list.append([
trade.id,
trade.pair + ('*' if (trade.open_order_id is not None
@@ -256,7 +259,7 @@ class RPC:
profitcol += " (" + fiat_display_currency + ")"
columns = ['ID', 'Pair', 'Since', profitcol]
return trades_list, columns
return trades_list, columns, fiat_profit_sum
def _rpc_daily_profit(
self, timescale: int,
@@ -554,7 +557,8 @@ class RPC:
if not fully_canceled:
# Get current rate and execute sell
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
self._freqtrade.execute_sell(trade, current_rate, sell_reason)
# ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING:
@@ -844,5 +848,7 @@ class RPC:
df_analyzed, arrow.Arrow.utcnow().datetime)
def _rpc_plot_config(self) -> Dict[str, Any]:
if (self._freqtrade.strategy.plot_config and
'subplots' not in self._freqtrade.strategy.plot_config):
self._freqtrade.strategy.plot_config['subplots'] = {}
return self._freqtrade.strategy.plot_config

View File

@@ -8,19 +8,21 @@ import logging
from datetime import timedelta
from html import escape
from itertools import chain
from typing import Any, Callable, Dict, List, Union
from math import isnan
from typing import Any, Callable, Dict, List, Optional, Union, cast
import arrow
from tabulate import tabulate
from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update
from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ParseMode,
ReplyKeyboardMarkup, Update)
from telegram.error import NetworkError, TelegramError
from telegram.ext import CallbackContext, CommandHandler, Updater
from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater
from telegram.utils.helpers import escape_markdown
from freqtrade.__init__ import __version__
from freqtrade.constants import DUST_PER_COIN
from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_coin_value
from freqtrade.misc import chunks, round_coin_value
from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType
@@ -87,7 +89,7 @@ class Telegram(RPCHandler):
Validates the keyboard configuration from telegram config
section.
"""
self._keyboard: List[List[Union[str, KeyboardButton]]] = [
self._keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = [
['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'],
['/count', '/start', '/stop', '/help']
@@ -169,6 +171,11 @@ class Telegram(RPCHandler):
[h.command for h in handles]
)
self._current_callback_query_handler: Optional[CallbackQueryHandler] = None
self._callback_query_handlers = {
'forcebuy': CallbackQueryHandler(self._forcebuy_inline)
}
def cleanup(self) -> None:
"""
Stops all running telegram threads.
@@ -354,19 +361,31 @@ class Telegram(RPCHandler):
:return: None
"""
try:
statlist, head = self._rpc._rpc_status_table(
self._config['stake_currency'], self._config.get('fiat_display_currency', ''))
fiat_currency = self._config.get('fiat_display_currency', '')
statlist, head, fiat_profit_sum = self._rpc._rpc_status_table(
self._config['stake_currency'], fiat_currency)
show_total = not isnan(fiat_profit_sum) and len(statlist) > 1
max_trades_per_msg = 50
"""
Calculate the number of messages of 50 trades per message
0.99 is used to make sure that there are no extra (empty) messages
As an example with 50 trades, there will be int(50/50 + 0.99) = 1 message
"""
for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)):
message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg],
messages_count = max(int(len(statlist) / max_trades_per_msg + 0.99), 1)
for i in range(0, messages_count):
trades = statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg]
if show_total and i == messages_count - 1:
# append total line
trades.append(["Total", "", "", f"{fiat_profit_sum:.2f} {fiat_currency}"])
message = tabulate(trades,
headers=head,
tablefmt='simple')
if show_total and i == messages_count - 1:
# insert separators line between Total
lines = message.split("\n")
message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]])
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e))
@@ -624,6 +643,25 @@ class Telegram(RPCHandler):
except RPCException as e:
self._send_msg(str(e))
def _forcebuy_action(self, pair, price=None):
try:
self._rpc._rpc_forcebuy(pair, price)
except RPCException as e:
self._send_msg(str(e))
def _forcebuy_inline(self, update: Update, _: CallbackContext) -> None:
if update.callback_query:
query = update.callback_query
pair = query.data
query.answer()
query.edit_message_text(text=f"Force Buying: {pair}")
self._forcebuy_action(pair)
@staticmethod
def _layout_inline_keyboard(buttons: List[InlineKeyboardButton],
cols=3) -> List[List[InlineKeyboardButton]]:
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
@authorized_only
def _forcebuy(self, update: Update, context: CallbackContext) -> None:
"""
@@ -636,10 +674,13 @@ class Telegram(RPCHandler):
if context.args:
pair = context.args[0]
price = float(context.args[1]) if len(context.args) > 1 else None
try:
self._rpc._rpc_forcebuy(pair, price)
except RPCException as e:
self._send_msg(str(e))
self._forcebuy_action(pair, price)
else:
whitelist = self._rpc._rpc_whitelist()['whitelist']
pairs = [InlineKeyboardButton(pair, callback_data=pair) for pair in whitelist]
self._send_inline_msg("Which pair?",
keyboard=self._layout_inline_keyboard(pairs),
callback_query_handler='forcebuy')
@authorized_only
def _trades(self, update: Update, context: CallbackContext) -> None:
@@ -711,8 +752,11 @@ class Telegram(RPCHandler):
trades = self._rpc._rpc_performance()
output = "<b>Performance:</b>\n"
for i, trade in enumerate(trades):
stat_line = (f"{i+1}.\t <code>{trade['pair']}\t{trade['profit']:.2f}% "
f"({trade['count']})</code>\n")
stat_line = (
f"{i+1}.\t <code>{trade['pair']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit']:.2f}%) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
@@ -750,17 +794,21 @@ class Telegram(RPCHandler):
Handler for /locks.
Returns the currently active locks
"""
locks = self._rpc._rpc_locks()
message = tabulate([[
lock['id'],
lock['pair'],
lock['lock_end_time'],
lock['reason']] for lock in locks['locks']],
headers=['ID', 'Pair', 'Until', 'Reason'],
tablefmt='simple')
message = f"<pre>{escape(message)}</pre>"
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML)
rpc_locks = self._rpc._rpc_locks()
if not rpc_locks['locks']:
self._send_msg('No active locks.', parse_mode=ParseMode.HTML)
for locks in chunks(rpc_locks['locks'], 25):
message = tabulate([[
lock['id'],
lock['pair'],
lock['lock_end_time'],
lock['reason']] for lock in locks],
headers=['ID', 'Pair', 'Until', 'Reason'],
tablefmt='simple')
message = f"<pre>{escape(message)}</pre>"
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _delete_locks(self, update: Update, context: CallbackContext) -> None:
@@ -860,9 +908,17 @@ class Telegram(RPCHandler):
"""
try:
edge_pairs = self._rpc._rpc_edge()
edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple')
message = f'<b>Edge only validated following pairs:</b>\n<pre>{edge_pairs_tab}</pre>'
self._send_msg(message, parse_mode=ParseMode.HTML)
if not edge_pairs:
message = '<b>Edge only validated following pairs:</b>'
self._send_msg(message, parse_mode=ParseMode.HTML)
for chunk in chunks(edge_pairs, 25):
edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple')
message = (f'<b>Edge only validated following pairs:</b>\n'
f'<pre>{edge_pairs_tab}</pre>')
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e))
@@ -959,8 +1015,9 @@ class Telegram(RPCHandler):
f"*Current state:* `{val['state']}`"
)
def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
disable_notification: bool = False) -> None:
def _send_inline_msg(self, msg: str, callback_query_handler,
parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False,
keyboard: List[List[InlineKeyboardButton]] = None, ) -> None:
"""
Send given markdown message
:param msg: message
@@ -968,7 +1025,29 @@ class Telegram(RPCHandler):
:param parse_mode: telegram parse mode
:return: None
"""
reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True)
if self._current_callback_query_handler:
self._updater.dispatcher.remove_handler(self._current_callback_query_handler)
self._current_callback_query_handler = self._callback_query_handlers[callback_query_handler]
self._updater.dispatcher.add_handler(self._current_callback_query_handler)
self._send_msg(msg, parse_mode, disable_notification,
cast(List[List[Union[str, KeyboardButton, InlineKeyboardButton]]], keyboard),
reply_markup=InlineKeyboardMarkup)
def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
disable_notification: bool = False,
keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = None,
reply_markup=ReplyKeyboardMarkup) -> None:
"""
Send given markdown message
:param msg: message
:param bot: alternative bot
:param parse_mode: telegram parse mode
:return: None
"""
if keyboard is None:
keyboard = self._keyboard
reply_markup = reply_markup(keyboard, resize_keyboard=True)
try:
try:
self._updater.bot.send_message(

View File

@@ -5,7 +5,9 @@ This module defines a base class for auto-hyperoptable strategies.
import logging
from abc import ABC, abstractmethod
from contextlib import suppress
from typing import Any, Dict, Iterator, Optional, Sequence, Tuple, Union
from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union
from freqtrade.optimize.hyperopt_tools import HyperoptTools
with suppress(ImportError):
@@ -26,7 +28,8 @@ class BaseParameter(ABC):
category: Optional[str]
default: Any
value: Any
hyperopt: bool = False
in_space: bool = False
name: str
def __init__(self, *, default: Any, space: Optional[str] = None,
optimize: bool = True, load: bool = True, **kwargs):
@@ -131,7 +134,7 @@ class IntParameter(NumericParameter):
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
calculating 100ds of indicators.
"""
if self.hyperopt:
if self.in_space and self.optimize:
# Scikit-optimize ranges are "inclusive", while python's "range" is exclusive
return range(self.low, self.high + 1)
else:
@@ -247,6 +250,10 @@ class HyperStrategyMixin(object):
"""
Initialize hyperoptable strategy mixin.
"""
self.config = config
self.ft_buy_params: List[BaseParameter] = []
self.ft_sell_params: List[BaseParameter] = []
self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT)
def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]:
@@ -257,15 +264,26 @@ class HyperStrategyMixin(object):
"""
if category not in ('buy', 'sell', None):
raise OperationalException('Category must be one of: "buy", "sell", None.')
if category is None:
params = self.ft_buy_params + self.ft_sell_params
else:
params = getattr(self, f"ft_{category}_params")
for par in params:
yield par.name, par
def _detect_parameters(self, category: str) -> Iterator[Tuple[str, BaseParameter]]:
""" Detect all parameters for 'category' """
for attr_name in dir(self):
if not attr_name.startswith('__'): # Ignore internals, not strictly necessary.
attr = getattr(self, attr_name)
if issubclass(attr.__class__, BaseParameter):
if (category and attr_name.startswith(category + '_')
if (attr_name.startswith(category + '_')
and attr.category is not None and attr.category != category):
raise OperationalException(
f'Inconclusive parameter name {attr_name}, category: {attr.category}.')
if (category is None or category == attr.category or
if (category == attr.category or
(attr_name.startswith(category + '_') and attr.category is None)):
yield attr_name, attr
@@ -283,9 +301,16 @@ class HyperStrategyMixin(object):
"""
if not params:
logger.info(f"No params for {space} found, using default values.")
param_container: List[BaseParameter] = getattr(self, f"ft_{space}_params")
for attr_name, attr in self._detect_parameters(space):
attr.name = attr_name
attr.in_space = hyperopt and HyperoptTools.has_space(self.config, space)
if not attr.category:
attr.category = space
param_container.append(attr)
for attr_name, attr in self.enumerate_parameters():
attr.hyperopt = hyperopt
if params and attr_name in params:
if attr.load:
attr.value = params[attr_name]
@@ -295,3 +320,16 @@ class HyperStrategyMixin(object):
f'Default value "{attr.value}" used.')
else:
logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}')
def get_params_dict(self):
"""
Returns list of Parameters that are not part of the current optimize job
"""
params = {
'buy': {},
'sell': {}
}
for name, p in self.enumerate_parameters():
if not p.optimize or not p.in_space:
params[p.category][name] = p.value
return params

View File

@@ -7,7 +7,7 @@ import warnings
from abc import ABC, abstractmethod
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Tuple
from typing import Dict, List, Optional, Tuple, Union
import arrow
from pandas import DataFrame
@@ -24,6 +24,7 @@ from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__)
CUSTOM_SELL_MAX_LENGTH = 64
class SignalType(Enum):
@@ -45,6 +46,7 @@ class SellType(Enum):
SELL_SIGNAL = "sell_signal"
FORCE_SELL = "force_sell"
EMERGENCY_SELL = "emergency_sell"
CUSTOM_SELL = "custom_sell"
NONE = ""
def __str__(self):
@@ -52,12 +54,20 @@ class SellType(Enum):
return self.value
class SellCheckTuple(NamedTuple):
class SellCheckTuple(object):
"""
NamedTuple for Sell type + reason
"""
sell_flag: bool
sell_type: SellType
sell_reason: str = ''
def __init__(self, sell_type: SellType, sell_reason: str = ''):
self.sell_type = sell_type
self.sell_reason = sell_reason or sell_type.value
@property
def sell_flag(self):
return self.sell_type != SellType.NONE
class IStrategy(ABC, HyperStrategyMixin):
@@ -151,6 +161,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""
return dataframe
@abstractmethod
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@@ -160,6 +171,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column
"""
return dataframe
@abstractmethod
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@@ -169,6 +181,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with sell column
"""
return dataframe
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
"""
@@ -216,7 +229,7 @@ class IStrategy(ABC, HyperStrategyMixin):
pass
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, **kwargs) -> bool:
time_in_force: str, current_time: datetime, **kwargs) -> bool:
"""
Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or
@@ -231,6 +244,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
@@ -238,7 +252,8 @@ class IStrategy(ABC, HyperStrategyMixin):
return True
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
rate: float, time_in_force: str, sell_reason: str,
current_time: datetime, **kwargs) -> bool:
"""
Called right before placing a regular sell order.
Timing for this function is critical, so avoid doing heavy computations or
@@ -257,6 +272,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param sell_reason: Sell reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell']
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange.
False aborts the process
@@ -285,6 +301,30 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return self.stoploss
def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
"""
Custom sell signal logic indicating that specified position should be sold. Returning a
string or True from this method is equal to setting sell signal on a candle at specified
time. This method is not called when sell signal is set.
This method should be overridden to create sell signals that depend on trade parameters. For
example you could implement a stoploss relative to candle when trade was opened, or a custom
1:2 risk-reward ROI.
Custom sell reason max length is 64. Exceeding this limit will raise OperationalException.
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: To execute sell, return a string with custom sell reason or True. Otherwise return
None or False.
"""
return None
def informative_pairs(self) -> ListPairsWithTimeframes:
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
@@ -531,12 +571,33 @@ class IStrategy(ABC, HyperStrategyMixin):
and self.min_roi_reached(trade=trade, current_profit=current_profit,
current_time=date))
sell_signal = SellType.NONE
custom_reason = ''
# use provided rate in backtesting, not high/low.
current_rate = rate
current_profit = trade.calc_profit_ratio(current_rate)
if (ask_strategy.get('sell_profit_only', False)
and current_profit <= ask_strategy.get('sell_profit_offset', 0)):
# sell_profit_only and profit doesn't reach the offset - ignore sell signal
sell_signal = False
else:
sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True)
pass
elif ask_strategy.get('use_sell_signal', True) and not buy:
if sell:
sell_signal = SellType.SELL_SIGNAL
else:
custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)(
pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate,
current_profit=current_profit)
if custom_reason:
sell_signal = SellType.CUSTOM_SELL
if isinstance(custom_reason, str):
if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH:
logger.warning(f'Custom sell reason returned from custom_sell is too '
f'long and was trimmed to {CUSTOM_SELL_MAX_LENGTH} '
f'characters.')
custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH]
else:
custom_reason = None
# TODO: return here if sell-signal should be favored over ROI
# Start evaluations
@@ -545,24 +606,23 @@ class IStrategy(ABC, HyperStrategyMixin):
# Sell-signal
# Stoploss
if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS:
logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, "
f"sell_type=SellType.ROI")
return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)
logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI")
return SellCheckTuple(sell_type=SellType.ROI)
if sell_signal:
logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, "
f"sell_type=SellType.SELL_SIGNAL")
return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)
if sell_signal != SellType.NONE:
logger.debug(f"{trade.pair} - Sell signal received. "
f"sell_type=SellType.{sell_signal.name}" +
(f", custom_reason={custom_reason}" if custom_reason else ""))
return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason)
if stoplossflag.sell_flag:
logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, "
f"sell_type={stoplossflag.sell_type}")
logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.sell_type}")
return stoplossflag
# This one is noisy, commented out...
# logger.debug(f"{trade.pair} - No sell signal. sell_flag=False")
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
# logger.debug(f"{trade.pair} - No sell signal.")
return SellCheckTuple(sell_type=SellType.NONE)
def stop_loss_reached(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float,
@@ -626,9 +686,9 @@ class IStrategy(ABC, HyperStrategyMixin):
logger.debug(f"{trade.pair} - Trailing stop saved "
f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
return SellCheckTuple(sell_flag=True, sell_type=sell_type)
return SellCheckTuple(sell_type=sell_type)
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
return SellCheckTuple(sell_type=SellType.NONE)
def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]:
"""

View File

@@ -9,7 +9,8 @@
"cancel_open_orders_on_exit": false,
"unfilledtimeout": {
"buy": 10,
"sell": 30
"sell": 30,
"unit": "minutes"
},
"bid_strategy": {
"price_side": "bid",

View File

@@ -14,8 +14,9 @@ def bot_loop_start(self, **kwargs) -> None:
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs) -> float:
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
current_rate: float, current_profit: float, dataframe: DataFrame,
**kwargs) -> float:
"""
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
e.g. returning -0.05 would create a stoploss 5% below current_rate.
@@ -31,13 +32,14 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', c
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the currentrate
"""
return self.stoploss
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, **kwargs) -> bool:
time_in_force: str, current_time: 'datetime', **kwargs) -> bool:
"""
Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or
@@ -52,6 +54,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
:param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
@@ -59,7 +62,8 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
return True
def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
rate: float, time_in_force: str, sell_reason: str,
current_time: 'datetime', **kwargs) -> bool:
"""
Called right before placing a regular sell order.
Timing for this function is critical, so avoid doing heavy computations or
@@ -78,6 +82,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
:param sell_reason: Sell reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell']
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange.
False aborts the process

View File

@@ -4,14 +4,14 @@
-r requirements-hyperopt.txt
coveralls==3.0.1
flake8==3.9.1
flake8==3.9.2
flake8-type-annotations==0.1.0
flake8-tidy-imports==4.2.1
flake8-tidy-imports==4.3.0
mypy==0.812
pytest==6.2.3
pytest==6.2.4
pytest-asyncio==0.15.1
pytest-cov==2.11.1
pytest-mock==3.6.0
pytest-cov==2.12.0
pytest-mock==3.6.1
pytest-random-order==1.0.4
isort==5.8.0

View File

@@ -3,7 +3,7 @@
# Required for hyperopt
scipy==1.6.3
scikit-learn==0.24.1
scikit-learn==0.24.2
scikit-optimize==0.8.1
filelock==3.0.12
joblib==1.0.1

View File

@@ -1,23 +1,23 @@
numpy==1.20.2
numpy==1.20.3
pandas==1.2.4
ccxt==1.48.76
ccxt==1.50.48
# Pin cryptography for now due to rust build errors with piwheels
cryptography==3.4.7
aiohttp==3.7.4.post0
SQLAlchemy==1.4.11
python-telegram-bot==13.4.1
arrow==1.0.3
cachetools==4.2.1
SQLAlchemy==1.4.15
python-telegram-bot==13.5
arrow==1.1.0
cachetools==4.2.2
requests==2.25.1
urllib3==1.26.4
wrapt==1.12.1
jsonschema==3.2.0
TA-Lib==0.4.19
technical==1.2.2
TA-Lib==0.4.20
technical==1.3.0
tabulate==0.8.9
pycoingecko==2.0.0
jinja2==2.11.3
jinja2==3.0.1
tables==3.6.1
blosc==1.10.2
@@ -31,10 +31,10 @@ python-rapidjson==1.0
sdnotify==0.3.2
# API Server
fastapi==0.63.0
fastapi==0.65.1
uvicorn==0.13.4
pyjwt==2.0.1
aiofiles==0.6.0
pyjwt==2.1.0
aiofiles==0.7.0
# Support for colorized terminal output
colorama==0.4.4

View File

@@ -396,7 +396,7 @@ def main(args):
sys.exit()
config = load_config(args['config'])
url = config.get('api_server', {}).get('server_url', '127.0.0.1')
url = config.get('api_server', {}).get('listen_ip_address', '127.0.0.1')
port = config.get('api_server', {}).get('listen_port', '8080')
username = config.get('api_server', {}).get('username')
password = config.get('api_server', {}).get('password')

View File

@@ -918,242 +918,244 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
captured.out)
def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results,
saved_hyperopt_results_legacy):
for _ in (saved_hyperopt_results, saved_hyperopt_results_legacy):
mocker.patch(
'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results',
MagicMock(return_value=saved_hyperopt_results_legacy)
)
args = [
"hyperopt-list",
"--no-details",
"--no-color",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12",
" 6/12", " 7/12", " 8/12", " 9/12", " 10/12",
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--best",
"--no-details",
"--no-color",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 5/12", " 10/12"])
assert all(x not in captured.out
for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-details",
"--no-color",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12", " 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-color",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params",
"Sell hyperspace params", "ROI table", "Stoploss"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--min-trades", "20",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"])
assert all(x not in captured.out
for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-details",
"--no-color",
"--max-trades", "20",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12", " 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-details",
"--no-color",
"--min-avg-profit", "0.11",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 10/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--max-avg-profit", "0.10",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12"])
assert all(x not in captured.out
for x in [" 2/12", " 4/12", " 10/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--min-total-profit", "0.4",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12",
" 9/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--max-total-profit", "0.4",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12",
" 9/12", " 11/12"])
assert all(x not in captured.out
for x in [" 4/12", " 10/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--min-objective", "0.1",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12",
" 9/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--max-objective", "0.1",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12",
" 9/12", " 11/12"])
assert all(x not in captured.out
for x in [" 4/12", " 10/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-details",
"--no-color",
"--min-avg-time", "2000",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12",
" 8/12", " 9/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--max-avg-time", "1500",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12", " 6/12"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12"
" 9/12", " 10/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--export-csv", "test_file.csv",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
log_has("CSV file created: test_file.csv", caplog)
f = Path("test_file.csv")
assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text()
assert f.is_file()
f.unlink()
def test_hyperopt_show(mocker, capsys, saved_hyperopt_results):
mocker.patch(
'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results',
MagicMock(return_value=hyperopt_results)
)
args = [
"hyperopt-list",
"--no-details",
"--no-color",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12",
" 6/12", " 7/12", " 8/12", " 9/12", " 10/12",
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--best",
"--no-details",
"--no-color",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 5/12", " 10/12"])
assert all(x not in captured.out
for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-details",
"--no-color",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12", " 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-color",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params",
"Sell hyperspace params", "ROI table", "Stoploss"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--min-trades", "20",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"])
assert all(x not in captured.out
for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-details",
"--no-color",
"--max-trades", "20",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12", " 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-details",
"--no-color",
"--min-avg-profit", "0.11",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 10/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--max-avg-profit", "0.10",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12"])
assert all(x not in captured.out
for x in [" 2/12", " 4/12", " 10/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--min-total-profit", "0.4",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12",
" 9/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--max-total-profit", "0.4",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12",
" 9/12", " 11/12"])
assert all(x not in captured.out
for x in [" 4/12", " 10/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--min-objective", "0.1",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12",
" 9/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--max-objective", "0.1",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12",
" 9/12", " 11/12"])
assert all(x not in captured.out
for x in [" 4/12", " 10/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-details",
"--no-color",
"--min-avg-time", "2000",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12",
" 8/12", " 9/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--max-avg-time", "1500",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12", " 6/12"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12"
" 9/12", " 10/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--export-csv", "test_file.csv",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
log_has("CSV file created: test_file.csv", caplog)
f = Path("test_file.csv")
assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text()
assert f.is_file()
f.unlink()
def test_hyperopt_show(mocker, capsys, hyperopt_results):
mocker.patch(
'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results',
MagicMock(return_value=hyperopt_results)
MagicMock(return_value=saved_hyperopt_results)
)
args = [

View File

@@ -3,7 +3,7 @@ import json
import logging
import re
from copy import deepcopy
from datetime import datetime
from datetime import datetime, timedelta
from functools import reduce
from pathlib import Path
from unittest.mock import MagicMock, Mock, PropertyMock
@@ -21,6 +21,7 @@ from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import LocalTrade, Trade, init_db
from freqtrade.resolvers import ExchangeResolver
from freqtrade.state import RunMode
from freqtrade.worker import Worker
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4,
mock_trade_5, mock_trade_6)
@@ -1677,6 +1678,7 @@ def buy_order_fee():
@pytest.fixture(scope="function")
def edge_conf(default_conf):
conf = deepcopy(default_conf)
conf['runmode'] = RunMode.DRY_RUN
conf['max_open_trades'] = -1
conf['tradable_balance_ratio'] = 0.5
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
@@ -1778,7 +1780,7 @@ def open_trade():
@pytest.fixture
def hyperopt_results():
def saved_hyperopt_results_legacy():
return [
{
'loss': 0.4366182531161519,
@@ -1907,3 +1909,136 @@ def hyperopt_results():
'is_best': False
}
]
@pytest.fixture
def saved_hyperopt_results():
return [
{
'loss': 0.4366182531161519,
'params_dict': {
'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501
'results_metrics': {'total_trades': 2, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'holding_avg': timedelta(minutes=3930.0)}, # noqa: E501
'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501
'total_profit': -0.00125625,
'current_epoch': 1,
'is_initial_point': True,
'is_best': True
}, {
'loss': 20.0,
'params_dict': {
'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501
'params_details': {
'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501
'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
'stoploss': {'stoploss': -0.338070047333259}},
'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 0, 'losses': 1, 'profit_mean': 0.012357, 'profit_median': -0.012222, 'profit_total': 6.185e-05, 'profit_total_abs': 0.12357, 'holding_avg': timedelta(minutes=1200.0)}, # noqa: E501
'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501
'total_profit': 6.185e-05,
'current_epoch': 2,
'is_initial_point': True,
'is_best': False
}, {
'loss': 14.241196856510731,
'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501
'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501
'results_metrics': {'total_trades': 621, 'wins': 320, 'draws': 0, 'losses': 301, 'profit_mean': -0.043883302093397747, 'profit_median': -0.012222, 'profit_total': -0.13639474, 'profit_total_abs': -272.515306, 'holding_avg': timedelta(minutes=1691.207729468599)}, # noqa: E501
'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501
'total_profit': -0.13639474,
'current_epoch': 3,
'is_initial_point': True,
'is_best': False
}, {
'loss': 100000,
'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501
'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501
'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False
}, {
'loss': 0.22195522184191518,
'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501
'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501
'results_metrics': {'total_trades': 14, 'wins': 6, 'draws': 0, 'losses': 8, 'profit_mean': -0.003539515, 'profit_median': -0.012222, 'profit_total': -0.002480140000000001, 'profit_total_abs': -4.955321, 'holding_avg': timedelta(minutes=3402.8571428571427)}, # noqa: E501
'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501
'total_profit': -0.002480140000000001,
'current_epoch': 5,
'is_initial_point': True,
'is_best': True
}, {
'loss': 0.545315889154162,
'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501
'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501
'results_metrics': {'total_trades': 39, 'wins': 20, 'draws': 0, 'losses': 19, 'profit_mean': -0.0021400679487179478, 'profit_median': -0.012222, 'profit_total': -0.0041773, 'profit_total_abs': -8.346264999999997, 'holding_avg': timedelta(minutes=636.9230769230769)}, # noqa: E501
'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501
'total_profit': -0.0041773,
'current_epoch': 6,
'is_initial_point': True,
'is_best': False
}, {
'loss': 4.713497421432944,
'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501
'params_details': {
'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
'results_metrics': {'total_trades': 318, 'wins': 100, 'draws': 0, 'losses': 218, 'profit_mean': -0.0039833954716981146, 'profit_median': -0.012222, 'profit_total': -0.06339929, 'profit_total_abs': -126.67197600000004, 'holding_avg': timedelta(minutes=3140.377358490566)}, # noqa: E501
'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501
'total_profit': -0.06339929,
'current_epoch': 7,
'is_initial_point': True,
'is_best': False
}, {
'loss': 20.0, # noqa: E501
'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501
'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501
'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 1, 'losses': 0, 'profit_mean': 0.0, 'profit_median': 0.0, 'profit_total': 0.0, 'profit_total_abs': 0.0, 'holding_avg': timedelta(minutes=5340.0)}, # noqa: E501
'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501
'total_profit': 0.0,
'current_epoch': 8,
'is_initial_point': True,
'is_best': False
}, {
'loss': 2.4731817780991223,
'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501
'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501
'results_metrics': {'total_trades': 229, 'wins': 150, 'draws': 0, 'losses': 79, 'profit_mean': -0.0038433433624454144, 'profit_median': -0.012222, 'profit_total': -0.044050070000000004, 'profit_total_abs': -88.01256299999999, 'holding_avg': timedelta(minutes=6505.676855895196)}, # noqa: E501
'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501
'total_profit': -0.044050070000000004, # noqa: E501
'current_epoch': 9,
'is_initial_point': True,
'is_best': False
}, {
'loss': -0.2604606005845212, # noqa: E501
'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501
'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501
'results_metrics': {'total_trades': 4, 'wins': 0, 'draws': 0, 'losses': 4, 'profit_mean': 0.001080385, 'profit_median': -0.012222, 'profit_total': 0.00021629, 'profit_total_abs': 0.432154, 'holding_avg': timedelta(minutes=2850.0)}, # noqa: E501
'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501
'total_profit': 0.00021629,
'current_epoch': 10,
'is_initial_point': True,
'is_best': True
}, {
'loss': 4.876465945994304, # noqa: E501
'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501
'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501
# New Hyperopt mode!
'results_metrics': {'total_trades': 117, 'wins': 67, 'draws': 0, 'losses': 50, 'profit_mean': -0.012698609145299145, 'profit_median': -0.012222, 'profit_total': -0.07436117, 'profit_total_abs': -148.573727, 'holding_avg': timedelta(minutes=4282.5641025641025)}, # noqa: E501
'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501
'total_profit': -0.07436117,
'current_epoch': 11,
'is_initial_point': True,
'is_best': False
}, {
'loss': 100000,
'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501
'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501
'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit_total_abs': 0.0, 'holding_avg': timedelta()}, # noqa: E501
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
'total_profit': 0,
'current_epoch': 12,
'is_initial_point': True,
'is_best': False
}
]

View File

@@ -246,3 +246,46 @@ def test_get_analyzed_dataframe(mocker, default_conf, ohlcv_history):
assert dataframe.empty
assert isinstance(time, datetime)
assert time == datetime(1970, 1, 1, tzinfo=timezone.utc)
# Test backtest mode
default_conf["runmode"] = RunMode.BACKTEST
dp._set_dataframe_max_index(1)
dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
assert len(dataframe) == 1
dp._set_dataframe_max_index(2)
dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
assert len(dataframe) == 2
dp._set_dataframe_max_index(3)
dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
assert len(dataframe) == 3
dp._set_dataframe_max_index(500)
dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
assert len(dataframe) == len(ohlcv_history)
def test_no_exchange_mode(default_conf):
dp = DataProvider(default_conf, None)
message = "Exchange is not available to DataProvider."
with pytest.raises(OperationalException, match=message):
dp.refresh([()])
with pytest.raises(OperationalException, match=message):
dp.ohlcv('XRP/USDT', '5m')
with pytest.raises(OperationalException, match=message):
dp.market('XRP/USDT')
with pytest.raises(OperationalException, match=message):
dp.ticker('XRP/USDT')
with pytest.raises(OperationalException, match=message):
dp.orderbook('XRP/USDT', 20)
with pytest.raises(OperationalException, match=message):
dp.available_pairs()

View File

@@ -344,6 +344,8 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog):
def test_edge_process_no_pairs(mocker, edge_conf, caplog):
edge_conf['exchange']['pair_whitelist'] = []
mocker.patch('freqtrade.freqtradebot.validate_config_consistency')
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001)
mocker.patch('freqtrade.edge.edge_positioning.refresh_data')

View File

@@ -2084,6 +2084,46 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
order_id='_', pair='TKN/BTC')
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', return_value={'for': 123})
mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order', return_value={'for': 123})
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
return_value={'fee': {}, 'status': 'canceled', 'amount': 1234})
mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order',
return_value={'fee': {}, 'status': 'canceled', 'amount': 1234})
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
assert co == {'fee': {}, 'status': 'canceled', 'amount': 1234}
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
return_value='canceled')
mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order',
return_value='canceled')
# Fall back to fetch_stoploss_order
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
assert co == {'for': 123}
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order',
side_effect=InvalidOrderException(""))
mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order',
side_effect=InvalidOrderException(""))
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
assert co['amount'] == 555
assert co == {'fee': {}, 'status': 'canceled', 'amount': 555, 'info': {}}
with pytest.raises(InvalidOrderException):
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
side_effect=InvalidOrderException("Did not find order"))
mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order',
side_effect=InvalidOrderException("Did not find order"))
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123)
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_order(default_conf, mocker, exchange_name):
default_conf['dry_run'] = True

View File

@@ -157,3 +157,26 @@ def test_fetch_stoploss_order(default_conf, mocker):
'fetch_stoploss_order', 'fetch_orders',
retries=API_FETCH_ORDER_RETRY_COUNT + 1,
order_id='_', pair='TKN/BTC')
def test_get_order_id(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
order = {
'type': STOPLOSS_ORDERTYPE,
'price': 1500,
'id': '1111',
'info': {
'orderId': '1234'
}
}
assert exchange.get_order_id_conditional(order) == '1234'
order = {
'type': 'limit',
'price': 1500,
'id': '1111',
'info': {
'orderId': '1234'
}
}
assert exchange.get_order_id_conditional(order) == '1111'

View File

@@ -90,6 +90,7 @@ def test_get_balances_prod(default_conf, mocker):
'3ST': balance_item.copy(),
'4ST': balance_item.copy(),
'EUR': balance_item.copy(),
'timestamp': 123123
})
kraken_open_orders = [{'symbol': '1ST/EUR',
'type': 'limit',
@@ -138,7 +139,7 @@ def test_get_balances_prod(default_conf, mocker):
default_conf['dry_run'] = False
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
balances = exchange.get_balances()
assert len(balances) == 5
assert len(balances) == 6
assert balances['1ST']['free'] == 9.0
assert balances['1ST']['total'] == 10.0

View File

@@ -185,7 +185,7 @@ tc11 = BTContainer(data=[
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5050, 4950, 5100, 6172, 0, 0],
[2, 5100, 5251, 5100, 5100, 6172, 0, 0],
[3, 4850, 5050, 4650, 4750, 6172, 0, 0],
[3, 5000, 5150, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.019, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
@@ -440,6 +440,23 @@ tc27 = BTContainer(data=[
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=4)]
)
# Test 28: trailing_stop should raise so candle 3 causes a stoploss
# Same case than tc11 - but candle 3 "gaps down" - the stoploss will be above the candle,
# therefore "open" will be used
# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2
tc28 = BTContainer(data=[
# D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5050, 4950, 5100, 6172, 0, 0],
[2, 5100, 5251, 5100, 5100, 6172, 0, 0],
[3, 4850, 5050, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.03, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
trailing_stop_positive=0.03,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
)
TESTS = [
tc0,
tc1,
@@ -469,6 +486,7 @@ TESTS = [
tc25,
tc26,
tc27,
tc28,
]
@@ -493,6 +511,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
patch_exchange(mocker)
frame = _build_backtest_dataframe(data.data)
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = lambda a, m: frame
backtesting.strategy.advise_sell = lambda a, m: frame
caplog.set_level(logging.DEBUG)
@@ -501,13 +520,14 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
# Dummy data as we mock the analyze functions
data_processed = {pair: frame.copy()}
min_date, max_date = get_timerange({pair: frame})
results = backtesting.backtest(
result = backtesting.backtest(
processed=data_processed,
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
results = result['results']
assert len(results) == len(data.trades)
assert round(results["profit_ratio"].sum(), 3) == round(data.profit_perc, 3)

View File

@@ -83,6 +83,7 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None:
patch_exchange(mocker)
config['timeframe'] = '1m'
backtesting = Backtesting(config)
backtesting._set_strategy(backtesting.strategylist[0])
data = load_data_test(contour, testdatadir)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
@@ -106,6 +107,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
data = trim_dictlist(data, -201)
patch_exchange(mocker)
backtesting = Backtesting(conf)
backtesting._set_strategy(backtesting.strategylist[0])
processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(processed)
return {
@@ -285,6 +287,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
patch_exchange(mocker)
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
assert backtesting.config == default_conf
assert backtesting.timeframe == '5m'
assert callable(backtesting.strategy.ohlcvdata_to_dataframe)
@@ -315,11 +318,13 @@ def test_data_with_fee(default_conf, mocker, testdatadir) -> None:
fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
assert backtesting.fee == 0.1234
assert fee_mock.call_count == 0
default_conf['fee'] = 0.0
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
assert backtesting.fee == 0.0
assert fee_mock.call_count == 0
@@ -330,6 +335,7 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
data = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
fill_up_missing=True)
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
assert len(processed['UNITTEST/BTC']) == 102
@@ -361,12 +367,13 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
default_conf['timerange'] = '-1510694220'
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.bot_loop_start = MagicMock()
backtesting.start()
# check the logs, that will contain the backtest result
exists = [
'Backtesting with data from 2017-11-14 21:17:00 '
'up to 2017-11-14 22:59:00 (0 days)..'
'up to 2017-11-14 22:59:00 (0 days).'
]
for line in exists:
assert log_has(line, caplog)
@@ -393,6 +400,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
default_conf['timerange'] = '20180101-20180102'
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
with pytest.raises(OperationalException, match='No data found. Terminating.'):
backtesting.start()
@@ -465,6 +473,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
default_conf['stake_amount'] = 'unlimited'
default_conf['max_open_trades'] = 2
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
pair = 'UNITTEST/BTC'
row = [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
@@ -508,19 +517,21 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
pair = 'UNITTEST/BTC'
timerange = TimeRange('date', None, 1517227800, 0)
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=timerange)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(processed)
results = backtesting.backtest(
result = backtesting.backtest(
processed=processed,
start_date=min_date,
end_date=max_date,
max_open_trades=10,
position_stacking=False,
)
results = result['results']
assert not results.empty
assert len(results) == 2
@@ -569,6 +580,7 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
# Run a backtesting for an exiting 1min timeframe
timerange = TimeRange.parse_timerange('1510688220-1510700340')
@@ -583,13 +595,14 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
max_open_trades=1,
position_stacking=False,
)
assert not results.empty
assert len(results) == 1
assert not results['results'].empty
assert len(results['results']) == 1
def test_processed(default_conf, mocker, testdatadir) -> None:
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
dict_of_tickerrows = load_data_test('raise', testdatadir)
dataframes = backtesting.strategy.ohlcvdata_to_dataframe(dict_of_tickerrows)
@@ -623,7 +636,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
# While buy-signals are unrealistic, running backtesting
# over and over again should not cause different results
for [contour, numres] in tests:
assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == numres
assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres
@pytest.mark.parametrize('protections,contour,expected', [
@@ -648,7 +661,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
# While buy-signals are unrealistic, running backtesting
# over and over again should not cause different results
assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == expected
assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == expected
def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
@@ -660,10 +673,11 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = fun # Override
backtesting.strategy.advise_sell = fun # Override
results = backtesting.backtest(**backtest_conf)
assert results.empty
result = backtesting.backtest(**backtest_conf)
assert result['results'].empty
def test_backtest_only_sell(mocker, default_conf, testdatadir):
@@ -675,10 +689,11 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = fun # Override
backtesting.strategy.advise_sell = fun # Override
results = backtesting.backtest(**backtest_conf)
assert results.empty
result = backtesting.backtest(**backtest_conf)
assert result['results'].empty
def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
@@ -688,12 +703,14 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
pair='UNITTEST/BTC', datadir=testdatadir)
default_conf['timeframe'] = '1m'
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = _trend_alternate # Override
backtesting.strategy.advise_sell = _trend_alternate # Override
results = backtesting.backtest(**backtest_conf)
result = backtesting.backtest(**backtest_conf)
# 200 candles in backtest data
# won't buy on first (shifted by 1)
# 100 buys signals
results = result['results']
assert len(results) == 100
# One trade was force-closed at the end
assert len(results.loc[results['is_open']]) == 0
@@ -729,6 +746,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
default_conf['timeframe'] = '5m'
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = _trend_alternate_hold # Override
backtesting.strategy.advise_sell = _trend_alternate_hold # Override
@@ -745,9 +763,9 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
results = backtesting.backtest(**backtest_conf)
# Make sure we have parallel trades
assert len(evaluate_result_multi(results, '5m', 2)) > 0
assert len(evaluate_result_multi(results['results'], '5m', 2)) > 0
# make sure we don't have trades with more than configured max_open_trades
assert len(evaluate_result_multi(results, '5m', 3)) == 0
assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0
backtest_conf = {
'processed': processed,
@@ -757,7 +775,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
'position_stacking': False,
}
results = backtesting.backtest(**backtest_conf)
assert len(evaluate_result_multi(results, '5m', 1)) == 0
assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0
def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
@@ -789,9 +807,9 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...',
'Loading data from 2017-11-14 20:57:00 '
'up to 2017-11-14 22:58:00 (0 days)..',
'up to 2017-11-14 22:58:00 (0 days).',
'Backtesting with data from 2017-11-14 21:17:00 '
'up to 2017-11-14 22:58:00 (0 days)..',
'up to 2017-11-14 22:58:00 (0 days).',
'Parameter --enable-position-stacking detected ...'
]
@@ -802,8 +820,20 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
@pytest.mark.filterwarnings("ignore:deprecated")
def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
default_conf['ask_strategy'].update({
"use_sell_signal": True,
"sell_profit_only": False,
"sell_profit_offset": 0.0,
"ignore_roi_if_buy_signal": False,
})
patch_exchange(mocker)
backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS))
backtestmock = MagicMock(return_value={
'results': pd.DataFrame(columns=BT_DATA_COLUMNS),
'config': default_conf,
'locks': [],
'rejected_signals': 20,
'final_balance': 1000,
})
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
@@ -817,7 +847,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
text_table_strategy=strattable_mock,
generate_pair_metrics=MagicMock(),
generate_sell_reason_stats=sell_reason_mock,
generate_strategy_metrics=strat_summary,
generate_strategy_comparison=strat_summary,
generate_daily_stats=MagicMock(),
)
patched_configuration_load_config_file(mocker, default_conf)
@@ -851,9 +881,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...',
'Loading data from 2017-11-14 20:57:00 '
'up to 2017-11-14 22:58:00 (0 days)..',
'up to 2017-11-14 22:58:00 (0 days).',
'Backtesting with data from 2017-11-14 21:17:00 '
'up to 2017-11-14 22:58:00 (0 days)..',
'up to 2017-11-14 22:58:00 (0 days).',
'Parameter --enable-position-stacking detected ...',
'Running backtesting for Strategy DefaultStrategy',
'Running backtesting for Strategy TestStrategyLegacy',
@@ -865,41 +895,60 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
@pytest.mark.filterwarnings("ignore:deprecated")
def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys):
default_conf['ask_strategy'].update({
"use_sell_signal": True,
"sell_profit_only": False,
"sell_profit_offset": 0.0,
"ignore_roi_if_buy_signal": False,
})
patch_exchange(mocker)
result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'],
'profit_ratio': [0.0, 0.0],
'profit_abs': [0.0, 0.0],
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00', ], utc=True
),
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00', ], utc=True),
'trade_duration': [235, 40],
'is_open': [False, False],
'stake_amount': [0.01, 0.01],
'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541],
'sell_reason': [SellType.ROI, SellType.ROI]
})
result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'],
'profit_ratio': [0.03, 0.01, 0.1],
'profit_abs': [0.01, 0.02, 0.2],
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00',
'2018-01-30 05:30:00'], utc=True
),
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00',
'2018-01-30 08:30:00'], utc=True),
'trade_duration': [47, 40, 20],
'is_open': [False, False, False],
'stake_amount': [0.01, 0.01, 0.01],
'open_rate': [0.104445, 0.10302485, 0.122541],
'close_rate': [0.104969, 0.103541, 0.123541],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
})
backtestmock = MagicMock(side_effect=[
pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'],
'profit_ratio': [0.0, 0.0],
'profit_abs': [0.0, 0.0],
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00', ], utc=True
),
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00', ], utc=True),
'trade_duration': [235, 40],
'is_open': [False, False],
'stake_amount': [0.01, 0.01],
'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541],
'sell_reason': [SellType.ROI, SellType.ROI]
}),
pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'],
'profit_ratio': [0.03, 0.01, 0.1],
'profit_abs': [0.01, 0.02, 0.2],
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00',
'2018-01-30 05:30:00'], utc=True
),
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00',
'2018-01-30 08:30:00'], utc=True),
'trade_duration': [47, 40, 20],
'is_open': [False, False, False],
'stake_amount': [0.01, 0.01, 0.01],
'open_rate': [0.104445, 0.10302485, 0.122541],
'close_rate': [0.104969, 0.103541, 0.123541],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
}),
{
'results': result1,
'config': default_conf,
'locks': [],
'rejected_signals': 20,
'final_balance': 1000,
},
{
'results': result2,
'config': default_conf,
'locks': [],
'rejected_signals': 20,
'final_balance': 1000,
}
])
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
@@ -930,9 +979,9 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...',
'Loading data from 2017-11-14 20:57:00 '
'up to 2017-11-14 22:58:00 (0 days)..',
'up to 2017-11-14 22:58:00 (0 days).',
'Backtesting with data from 2017-11-14 21:17:00 '
'up to 2017-11-14 22:58:00 (0 days)..',
'up to 2017-11-14 22:58:00 (0 days).',
'Parameter --enable-position-stacking detected ...',
'Running backtesting for Strategy DefaultStrategy',
'Running backtesting for Strategy TestStrategyLegacy',

View File

@@ -5,7 +5,7 @@ import re
from datetime import datetime
from pathlib import Path
from typing import Dict, List
from unittest.mock import MagicMock
from unittest.mock import ANY, MagicMock
import pandas as pd
import pytest
@@ -18,10 +18,12 @@ from freqtrade.exceptions import OperationalException
from freqtrade.optimize.hyperopt import Hyperopt
from freqtrade.optimize.hyperopt_auto import HyperOptAuto
from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.optimize.optimize_reports import generate_strategy_stats
from freqtrade.optimize.space import SKDecimal
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
from freqtrade.state import RunMode
from freqtrade.strategy.hyper import IntParameter
from freqtrade.strategy.interface import SellType
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file)
@@ -29,23 +31,7 @@ from .hyperopts.default_hyperopt import DefaultHyperOpt
# Functions for recurrent object patching
def create_results(mocker, hyperopt, testdatadir) -> List[Dict]:
"""
When creating results, mock the hyperopt so that *by default*
- we don't create any pickle'd files in the filesystem
- we might have a pickle'd file so make sure that we return
false when looking for it
"""
hyperopt.results_file = testdatadir / 'optimize/ut_results.pickle'
mocker.patch.object(Path, "is_file", MagicMock(return_value=False))
stat_mock = MagicMock()
stat_mock.st_size = 1
mocker.patch.object(Path, "stat", MagicMock(return_value=stat_mock))
mocker.patch.object(Path, "unlink", MagicMock(return_value=True))
mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
def create_results() -> List[Dict]:
return [{'loss': 1, 'result': 'foo', 'params': {}, 'is_best': True}]
@@ -319,54 +305,49 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
assert caplog.record_tuples == []
def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> None:
epochs = create_results(mocker, hyperopt, testdatadir)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
mock_dump_json = mocker.patch('freqtrade.optimize.hyperopt.file_dump_json', return_value=None)
results_file = testdatadir / 'optimize' / 'ut_results.pickle'
def test_save_results_saves_epochs(mocker, hyperopt, tmpdir, caplog) -> None:
# Test writing to temp dir and reading again
epochs = create_results()
hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt')
caplog.set_level(logging.DEBUG)
hyperopt.epochs = epochs
hyperopt._save_results()
assert log_has(f"1 epoch saved to '{results_file}'.", caplog)
mock_dump.assert_called_once()
mock_dump_json.assert_called_once()
for epoch in epochs:
hyperopt._save_result(epoch)
assert log_has(f"1 epoch saved to '{hyperopt.results_file}'.", caplog)
hyperopt.epochs = epochs + epochs
hyperopt._save_results()
assert log_has(f"2 epochs saved to '{results_file}'.", caplog)
hyperopt._save_result(epochs[0])
assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog)
hyperopt_epochs = HyperoptTools.load_previous_results(hyperopt.results_file)
assert len(hyperopt_epochs) == 2
def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None:
epochs = create_results(mocker, hyperopt, testdatadir)
mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs)
results_file = testdatadir / 'optimize' / 'ut_results.pickle'
hyperopt_epochs = HyperoptTools._read_results(results_file)
assert log_has(f"Reading epochs from '{results_file}'", caplog)
assert hyperopt_epochs == epochs
mock_load.assert_called_once()
def test_load_previous_results(testdatadir, caplog) -> None:
def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None:
epochs = create_results(mocker, hyperopt, testdatadir)
mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs)
mocker.patch.object(Path, 'is_file', MagicMock(return_value=True))
statmock = MagicMock()
statmock.st_size = 5
# mocker.patch.object(Path, 'stat', MagicMock(return_value=statmock))
results_file = testdatadir / 'optimize' / 'ut_results.pickle'
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert hyperopt_epochs == epochs
mock_load.assert_called_once()
assert len(hyperopt_epochs) == 5
assert log_has_re(r"Reading pickled epochs from .*", caplog)
del epochs[0]['is_best']
mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs)
caplog.clear()
with pytest.raises(OperationalException):
# Modern version
results_file = testdatadir / 'strategy_SampleStrategy.fthypt'
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert len(hyperopt_epochs) == 5
assert log_has_re(r"Reading epochs from .*", caplog)
def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results_pickle',
return_value=[{'asdf': '222'}])
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
with pytest.raises(OperationalException, match=r"The file .* incompatible.*"):
HyperoptTools.load_previous_results(results_file)
@@ -384,7 +365,8 @@ def test_roi_table_generation(hyperopt) -> None:
def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
@@ -423,9 +405,9 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
# Should be called for historical candle data
assert dumper.call_count == 1
assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
assert hasattr(hyperopt, "max_open_trades")
@@ -433,18 +415,42 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
assert hasattr(hyperopt, "position_stacking")
def test_format_results(hyperopt):
# Test with BTC as stake_currency
trades = [
('ETH/BTC', 2, 2, 123),
('LTC/BTC', 1, 1, 123),
('XPR/BTC', -1, -2, -246)
]
labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration']
df = pd.DataFrame.from_records(trades, columns=labels)
results_metrics = hyperopt._calculate_results_metrics(df)
results_explanation = hyperopt._format_results_explanation_string(results_metrics)
total_profit = results_metrics['total_profit']
def test_hyperopt_format_results(hyperopt):
bt_result = {
'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
"UNITTEST/BTC", "UNITTEST/BTC"],
"profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780],
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
"open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime,
Arrow(2017, 11, 14, 22, 12, 00).datetime,
Arrow(2017, 11, 14, 22, 44, 00).datetime],
"close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime,
Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 00).datetime,
Arrow(2017, 11, 14, 22, 58, 00).datetime],
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
"trade_duration": [123, 34, 31, 14],
"is_open": [False, False, False, True],
"stake_amount": [0.01, 0.01, 0.01, 0.01],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL]
}),
'config': hyperopt.config,
'locks': [],
'final_balance': 0.02,
'rejected_signals': 2,
'backtest_start_time': 1619718665,
'backtest_end_time': 1619718665,
}
results_metrics = generate_strategy_stats({'XRP/BTC': None}, '', bt_result,
Arrow(2017, 11, 14, 19, 32, 00),
Arrow(2017, 12, 14, 19, 32, 00), market_change=0)
results_explanation = HyperoptTools.format_results_explanation_string(results_metrics, 'BTC')
total_profit = results_metrics['profit_total_abs']
results = {
'loss': 0.0,
@@ -458,21 +464,9 @@ def test_format_results(hyperopt):
}
result = HyperoptTools._format_explanation_string(results, 1)
assert result.find(' 66.67%')
assert result.find('Total profit 1.00000000 BTC')
assert result.find('2.0000Σ %')
# Test with EUR as stake_currency
trades = [
('ETH/EUR', 2, 2, 123),
('LTC/EUR', 1, 1, 123),
('XPR/EUR', -1, -2, -246)
]
df = pd.DataFrame.from_records(trades, columns=labels)
results_metrics = hyperopt._calculate_results_metrics(df)
results['total_profit'] = results_metrics['total_profit']
result = HyperoptTools._format_explanation_string(results, 1)
assert result.find('Total profit 1.00000000 EUR')
assert ' 0.71%' in result
assert 'Total profit 0.00003100 BTC' in result
assert '0:50:00 min' in result
@pytest.mark.parametrize("spaces, expected_results", [
@@ -503,10 +497,10 @@ def test_format_results(hyperopt):
(['default', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
])
def test_has_space(hyperopt, spaces, expected_results):
def test_has_space(hyperopt_conf, spaces, expected_results):
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
hyperopt.config.update({'spaces': spaces})
assert hyperopt.has_space(s) == expected_results[s]
hyperopt_conf.update({'spaces': spaces})
assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
def test_populate_indicators(hyperopt, testdatadir) -> None:
@@ -577,22 +571,39 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
'hyperopt_min_trades': 1,
})
trades = [
('TRX/BTC', 0.023117, 0.000233, 100)
]
labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration']
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
backtest_result = {
'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
"UNITTEST/BTC", "UNITTEST/BTC"],
"profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780],
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
"open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime,
Arrow(2017, 11, 14, 22, 12, 00).datetime,
Arrow(2017, 11, 14, 22, 44, 00).datetime],
"close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime,
Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 00).datetime,
Arrow(2017, 11, 14, 22, 58, 00).datetime],
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
"trade_duration": [123, 34, 31, 14],
"is_open": [False, False, False, True],
"stake_amount": [0.01, 0.01, 0.01, 0.01],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL]
}),
'config': hyperopt_conf,
'locks': [],
'rejected_signals': 20,
'final_balance': 1000,
}
mocker.patch(
'freqtrade.optimize.hyperopt.Backtesting.backtest',
MagicMock(return_value=backtest_result)
)
mocker.patch(
'freqtrade.optimize.hyperopt.get_timerange',
MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13)))
)
mocker.patch('freqtrade.optimize.hyperopt.Backtesting.backtest', return_value=backtest_result)
mocker.patch('freqtrade.optimize.hyperopt.get_timerange',
return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13)))
patch_exchange(mocker)
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
mocker.patch.object(Path, 'open')
mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None})
optimizer_param = {
'adx-value': 0,
@@ -626,11 +637,11 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
'trailing_only_offset_is_reached': False,
}
response_expected = {
'loss': 1.9840569076926293,
'results_explanation': (' 1 trades. 1/0/0 Wins/Draws/Losses. '
'Avg profit 2.31%. Median profit 2.31%. Total profit '
'0.00023300 BTC ( 2.31\N{GREEK CAPITAL LETTER SIGMA}%). '
'Avg duration 100.0 min.'
'loss': 1.9147239021396234,
'results_explanation': (' 4 trades. 4/0/0 Wins/Draws/Losses. '
'Avg profit 0.77%. Median profit 0.71%. Total profit '
'0.00003100 BTC ( 0.00\N{GREEK CAPITAL LETTER SIGMA}%). '
'Avg duration 0:50:00 min.'
).encode(locale.getpreferredencoding(), 'replace').decode('utf-8'),
'params_details': {'buy': {'adx-enabled': False,
'adx-value': 0,
@@ -641,10 +652,10 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
'rsi-enabled': False,
'rsi-value': 0,
'trigger': 'macd_cross_signal'},
'roi': {0: 0.12000000000000001,
20.0: 0.02,
50.0: 0.01,
110.0: 0},
'roi': {"0": 0.12000000000000001,
"20.0": 0.02,
"50.0": 0.01,
"110.0": 0},
'sell': {'sell-adx-enabled': False,
'sell-adx-value': 0,
'sell-fastd-enabled': True,
@@ -660,21 +671,16 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
'trailing_stop_positive': 0.02,
'trailing_stop_positive_offset': 0.07}},
'params_dict': optimizer_param,
'results_metrics': {'avg_profit': 2.3117,
'draws': 0,
'duration': 100.0,
'losses': 0,
'winsdrawslosses': ' 1 0 0',
'median_profit': 2.3117,
'profit': 2.3117,
'total_profit': 0.000233,
'trade_count': 1,
'wins': 1},
'total_profit': 0.00023300
'params_not_optimized': {'buy': {}, 'sell': {}},
'results_metrics': ANY,
'total_profit': 3.1e-08
}
hyperopt = Hyperopt(hyperopt_conf)
hyperopt.dimensions = hyperopt.hyperopt_space()
hyperopt.min_date = Arrow(2017, 12, 10)
hyperopt.max_date = Arrow(2017, 12, 13)
hyperopt.init_spaces()
hyperopt.dimensions = hyperopt.dimensions
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
assert generate_optimizer_value == response_expected
@@ -691,7 +697,8 @@ def test_clean_hyperopt(mocker, hyperopt_conf, caplog):
def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
@@ -742,13 +749,14 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
':{},"stoploss":null,"trailing_stop":null}'
)
assert result_str in out # noqa: E501
assert dumper.called
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
# Should be called for historical candle data
assert dumper.call_count == 1
assert dumper2.call_count == 1
def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
@@ -790,13 +798,14 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None:
out, err = capsys.readouterr()
assert '{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501
assert dumper.called
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
# Should be called for historical candle data
assert dumper.call_count == 1
assert dumper2.call_count == 1
def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
@@ -837,13 +846,14 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
out, err = capsys.readouterr()
assert '{"minimal_roi":{},"stoploss":null}' in out
assert dumper.called
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
assert dumper.call_count == 1
assert dumper2.call_count == 1
def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
@@ -885,9 +895,9 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
assert dumper.call_count == 1
assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
assert hasattr(hyperopt, "max_open_trades")
@@ -923,7 +933,8 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None:
def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
@@ -966,8 +977,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
assert dumper.call_count == 1
assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
assert hasattr(hyperopt, "max_open_trades")
@@ -976,7 +987,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
@@ -1019,8 +1031,8 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
assert dumper.call_count == 1
assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
assert hasattr(hyperopt, "max_open_trades")
@@ -1108,7 +1120,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
assert hyperopt.backtesting.strategy.buy_rsi.hyperopt is True
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
assert isinstance(buy_rsi_range, range)
@@ -1133,3 +1145,17 @@ def test_SKDecimal():
assert space.transform([2.0]) == [200]
assert space.transform([1.0]) == [100]
assert space.transform([1.5, 1.6]) == [150, 160]
def test___pprint():
params = {'buy_std': 1.2, 'buy_rsi': 31, 'buy_enable': True, 'buy_what': 'asdf'}
non_params = {'buy_notoptimied': 55}
x = HyperoptTools._pprint(params, non_params)
assert x == """{
"buy_std": 1.2,
"buy_rsi": 31,
"buy_enable": True,
"buy_what": "asdf",
"buy_notoptimied": 55, # value loaded from strategy
}"""

View File

@@ -1,5 +1,6 @@
import datetime
import re
from datetime import datetime, timedelta, timezone
from datetime import timedelta
from pathlib import Path
import pandas as pd
@@ -7,14 +8,15 @@ import pytest
from arrow import Arrow
from freqtrade.configuration import TimeRange
from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
from freqtrade.data import history
from freqtrade.data.btanalysis import get_latest_backtest_filename, load_backtest_data
from freqtrade.edge import PairInfo
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats,
generate_edge_table, generate_pair_metrics,
generate_sell_reason_stats,
generate_strategy_metrics, store_backtest_stats,
generate_strategy_comparison,
generate_trading_stats, store_backtest_stats,
text_table_bt_results, text_table_sell_reason,
text_table_strategy)
from freqtrade.resolvers.strategy_resolver import StrategyResolver
@@ -26,25 +28,22 @@ def test_text_table_bt_results():
results = pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC'],
'profit_ratio': [0.1, 0.2],
'profit_abs': [0.2, 0.4],
'trade_duration': [10, 30],
'wins': [2, 0],
'draws': [0, 0],
'losses': [0, 0]
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
'profit_ratio': [0.1, 0.2, -0.05],
'profit_abs': [0.2, 0.4, -0.1],
'trade_duration': [10, 30, 20],
}
)
result_str = (
'| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |'
' Tot Profit % | Avg Duration | Wins | Draws | Losses |\n'
'|---------+--------+----------------+----------------+------------------+'
'----------------+----------------+--------+---------+----------|\n'
'| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 |'
' 15.00 | 0:20:00 | 2 | 0 | 0 |\n'
'| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 |'
' 15.00 | 0:20:00 | 2 | 0 | 0 |'
'| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |'
' Avg Duration | Win Draw Loss Win% |\n'
'|---------+--------+----------------+----------------+------------------+----------------+'
'----------------+-------------------------|\n'
'| ETH/BTC | 3 | 8.33 | 25.00 | 0.50000000 | 12.50 |'
' 0:20:00 | 2 0 1 66.7 |\n'
'| TOTAL | 3 | 8.33 | 25.00 | 0.50000000 | 12.50 |'
' 0:20:00 | 2 0 1 66.7 |'
)
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
@@ -80,6 +79,7 @@ def test_generate_backtest_stats(default_conf, testdatadir):
'config': default_conf,
'locks': [],
'final_balance': 1000.02,
'rejected_signals': 20,
'backtest_start_time': Arrow.utcnow().int_timestamp,
'backtest_end_time': Arrow.utcnow().int_timestamp,
}
@@ -96,8 +96,8 @@ def test_generate_backtest_stats(default_conf, testdatadir):
assert 'DefStrat' in stats['strategy']
assert 'strategy_comparison' in stats
strat_stats = stats['strategy']['DefStrat']
assert strat_stats['backtest_start'] == min_date.datetime
assert strat_stats['backtest_end'] == max_date.datetime
assert strat_stats['backtest_start'] == min_date.strftime(DATETIME_PRINT_FORMAT)
assert strat_stats['backtest_end'] == max_date.strftime(DATETIME_PRINT_FORMAT)
assert strat_stats['total_trades'] == len(results['DefStrat']['results'])
# Above sample had no loosing trade
assert strat_stats['max_drawdown'] == 0.0
@@ -127,6 +127,7 @@ def test_generate_backtest_stats(default_conf, testdatadir):
'config': default_conf,
'locks': [],
'final_balance': 1000.02,
'rejected_signals': 20,
'backtest_start_time': Arrow.utcnow().int_timestamp,
'backtest_end_time': Arrow.utcnow().int_timestamp,
}
@@ -140,8 +141,8 @@ def test_generate_backtest_stats(default_conf, testdatadir):
strat_stats = stats['strategy']['DefStrat']
assert strat_stats['max_drawdown'] == 0.013803
assert strat_stats['drawdown_start'] == datetime(2017, 11, 14, 22, 10, tzinfo=timezone.utc)
assert strat_stats['drawdown_end'] == datetime(2017, 11, 14, 22, 43, tzinfo=timezone.utc)
assert strat_stats['drawdown_start'] == '2017-11-14 22:10:00'
assert strat_stats['drawdown_end'] == '2017-11-14 22:43:00'
assert strat_stats['drawdown_end_ts'] == 1510699380000
assert strat_stats['drawdown_start_ts'] == 1510697400000
assert strat_stats['pairlist'] == ['UNITTEST/BTC']
@@ -226,8 +227,6 @@ def test_generate_daily_stats(testdatadir):
assert res['winning_days'] == 14
assert res['draw_days'] == 4
assert res['losing_days'] == 3
assert res['winner_holding_avg'] == timedelta(seconds=1440)
assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420)
# Select empty dataframe!
res = generate_daily_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :])
@@ -238,6 +237,23 @@ def test_generate_daily_stats(testdatadir):
assert res['losing_days'] == 0
def test_generate_trading_stats(testdatadir):
filename = testdatadir / "backtest-result_new.json"
bt_data = load_backtest_data(filename)
res = generate_trading_stats(bt_data)
assert isinstance(res, dict)
assert res['winner_holding_avg'] == timedelta(seconds=1440)
assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420)
assert 'wins' in res
assert 'losses' in res
assert 'draws' in res
# Select empty dataframe!
res = generate_trading_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :])
assert res['wins'] == 0
assert res['losses'] == 0
def test_text_table_sell_reason():
results = pd.DataFrame(
@@ -254,14 +270,14 @@ def test_text_table_sell_reason():
)
result_str = (
'| Sell Reason | Sells | Wins | Draws | Losses |'
' Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |\n'
'|---------------+---------+--------+---------+----------+'
'----------------+----------------+------------------+----------------|\n'
'| roi | 2 | 2 | 0 | 0 |'
' 15 | 30 | 0.6 | 15 |\n'
'| stop_loss | 1 | 0 | 0 | 1 |'
' -10 | -10 | -0.2 | -5 |'
'| Sell Reason | Sells | Win Draws Loss Win% | Avg Profit % | Cum Profit % |'
' Tot Profit BTC | Tot Profit % |\n'
'|---------------+---------+--------------------------+----------------+----------------+'
'------------------+----------------|\n'
'| roi | 2 | 2 0 0 100 | 15 | 30 |'
' 0.6 | 15 |\n'
'| stop_loss | 1 | 0 0 1 0 | -10 | -10 |'
' -0.2 | -5 |'
)
sell_reason_stats = generate_sell_reason_stats(max_open_trades=2,
@@ -309,9 +325,12 @@ def test_text_table_strategy(default_conf):
default_conf['max_open_trades'] = 2
default_conf['dry_run_wallet'] = 3
results = {}
date = datetime.datetime(year=2020, month=1, day=1, hour=12, minute=30)
delta = datetime.timedelta(days=1)
results['TestStrategy1'] = {'results': pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
'close_date': [date, date + delta, date + delta * 2],
'profit_ratio': [0.1, 0.2, 0.3],
'profit_abs': [0.2, 0.4, 0.5],
'trade_duration': [10, 30, 10],
@@ -324,6 +343,7 @@ def test_text_table_strategy(default_conf):
results['TestStrategy2'] = {'results': pd.DataFrame(
{
'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'],
'close_date': [date, date + delta, date + delta * 2],
'profit_ratio': [0.4, 0.2, 0.3],
'profit_abs': [0.4, 0.4, 0.5],
'trade_duration': [15, 30, 15],
@@ -335,18 +355,17 @@ def test_text_table_strategy(default_conf):
), 'config': default_conf}
result_str = (
'| Strategy | Buys | Avg Profit % | Cum Profit % | Tot'
' Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses |\n'
'| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |'
' Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n'
'|---------------+--------+----------------+----------------+------------------+'
'----------------+----------------+--------+---------+----------|\n'
'----------------+----------------+-------------------------+-----------------------|\n'
'| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |'
' 36.67 | 0:17:00 | 3 | 0 | 0 |\n'
' 36.67 | 0:17:00 | 3 0 0 100 | 0.00000000 BTC 0.00% |\n'
'| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |'
' 43.33 | 0:20:00 | 3 | 0 | 0 |'
' 43.33 | 0:20:00 | 3 0 0 100 | 0.00000000 BTC 0.00% |'
)
strategy_results = generate_strategy_metrics(all_results=results)
strategy_results = generate_strategy_comparison(all_results=results)
assert text_table_strategy(strategy_results, 'BTC') == result_str

View File

@@ -7,10 +7,11 @@ import pytest
from freqtrade.constants import AVAILABLE_PAIRLISTS
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import Trade
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.resolvers import PairListResolver
from tests.conftest import get_patched_freqtradebot, log_has, log_has_re
from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re
@pytest.fixture(scope="function")
@@ -406,6 +407,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "low_price_ratio": 0.02}],
"USDT", ['ETH/USDT', 'NANO/USDT']),
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "max_value": 0.000001}],
"USDT", ['NANO/USDT']),
([{"method": "StaticPairList"},
{"method": "RangeStabilityFilter", "lookback_days": 10,
"min_rate_of_change": 0.01, "refresh_period": 1440}],
@@ -488,6 +492,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
r'because last price < .*%$', caplog) or
log_has_re(r'^Removed .* from whitelist, '
r'because last price > .*%$', caplog) or
log_has_re(r'^Removed .* from whitelist, '
r'because min value change of .*', caplog) or
log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] "
r"is empty.*", caplog))
if pairlist['method'] == 'VolumePairList':
@@ -512,6 +518,18 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None:
PairListManager(MagicMock, whitelist_conf)
def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}]
if hasattr(Trade, 'query'):
del Trade.query
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
exchange = get_patched_exchange(mocker, whitelist_conf)
pm = PairListManager(exchange, whitelist_conf)
pm.refresh_pairlist()
assert log_has("PerformanceFilter is not available in this mode.", caplog)
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}]
@@ -787,6 +805,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]",
None
),
({"method": "PriceFilter", "max_value": 0.00002000},
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced Value above 0.00002000.'}]",
None
),
({"method": "PriceFilter"},
"[{'PriceFilter': 'PriceFilter - No price filters configured.'}]",
None
@@ -803,6 +825,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
None,
"PriceFilter requires max_price to be >= 0"
), # OperationalException expected
({"method": "PriceFilter", "max_value": -1.00010000},
None,
"PriceFilter requires max_value to be >= 0"
), # OperationalException expected
({"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01},
"[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate of change below "
"0.01 over the last days.'}]",

View File

@@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors,
# pragma pylint: disable=protected-access, C0103
import datetime
from unittest.mock import MagicMock
import pytest
@@ -21,6 +22,12 @@ def test_fiat_convert_is_supported(mocker):
def test_fiat_convert_find_price(mocker):
fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {}
fiat_convert._backoff = 0
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._load_cryptomap',
return_value=None)
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 0.0
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')
@@ -115,6 +122,28 @@ def test_fiat_convert_without_network(mocker):
CryptoToFiatConverter._coingekko = cmc_temp
def test_fiat_too_many_requests_response(mocker, caplog):
# Because CryptoToFiatConverter is a Singleton we reset the listings
req_exception = "429 Too Many Requests"
listmock = MagicMock(return_value="{}", side_effect=RequestException(req_exception))
mocker.patch.multiple(
'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
get_coins_list=listmock,
)
# with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {}
fiat_convert._load_cryptomap()
length_cryptomap = len(fiat_convert._cryptomap)
assert length_cryptomap == 0
assert fiat_convert._backoff > datetime.datetime.now().timestamp()
assert log_has(
'Too many requests for Coingecko API, backing off and trying again later.',
caplog
)
def test_fiat_invalid_response(mocker, caplog):
# Because CryptoToFiatConverter is a Singleton we reset the listings
listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}")

View File

@@ -199,28 +199,31 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
freqtradebot.enter_positions()
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert "Since" in headers
assert "Pair" in headers
assert 'instantly' == result[0][2]
assert 'ETH/BTC' in result[0][1]
assert '-0.41%' == result[0][3]
assert isnan(fiat_profit_sum)
# Test with fiatconvert
rpc._fiat_converter = CryptoToFiatConverter()
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert "Since" in headers
assert "Pair" in headers
assert 'instantly' == result[0][2]
assert 'ETH/BTC' in result[0][1]
assert '-0.41% (-0.06)' == result[0][3]
assert '-0.06' == f'{fiat_profit_sum:.2f}'
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert 'instantly' == result[0][2]
assert 'ETH/BTC' in result[0][1]
assert 'nan%' == result[0][3]
assert isnan(fiat_profit_sum)
def test_rpc_daily_profit(default_conf, update, ticker, fee,

View File

@@ -710,7 +710,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,):
assert 'draws' in rc.json()['durations']
def test_api_performance(botclient, mocker, ticker, fee):
def test_api_performance(botclient, fee):
ftbot, client = botclient
patch_get_signal(ftbot, (True, False))
@@ -728,6 +728,7 @@ def test_api_performance(botclient, mocker, ticker, fee):
)
trade.close_profit = trade.calc_profit_ratio()
trade.close_profit_abs = trade.calc_profit()
Trade.query.session.add(trade)
trade = Trade(
@@ -743,14 +744,16 @@ def test_api_performance(botclient, mocker, ticker, fee):
close_rate=0.391
)
trade.close_profit = trade.calc_profit_ratio()
trade.close_profit_abs = trade.calc_profit()
Trade.query.session.add(trade)
Trade.query.session.flush()
rc = client_get(client, f"{BASE_URI}/performance")
assert_response(rc)
assert len(rc.json()) == 2
assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61},
{'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}]
assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61, 'profit_abs': 0.01872279},
{'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}]
def test_api_status(botclient, mocker, ticker, fee, markets):
@@ -1142,6 +1145,14 @@ def test_api_plot_config(botclient):
assert_response(rc)
assert rc.json() == ftbot.strategy.plot_config
assert isinstance(rc.json()['main_plot'], dict)
assert isinstance(rc.json()['subplots'], dict)
ftbot.strategy.plot_config = {'main_plot': {'sma': {}}}
rc = client_get(client, f"{BASE_URI}/plot_config")
assert_response(rc)
assert isinstance(rc.json()['main_plot'], dict)
assert isinstance(rc.json()['subplots'], dict)
def test_api_strategies(botclient):

View File

@@ -4,6 +4,7 @@
import re
from datetime import datetime
from functools import reduce
from random import choice, randint
from string import ascii_uppercase
from unittest.mock import ANY, MagicMock
@@ -54,6 +55,14 @@ class DummyCls(Telegram):
raise Exception('test')
def get_telegram_testobject_with_inline(mocker, default_conf, mock=True, ftbot=None):
inline_msg_mock = MagicMock()
telegram, ftbot, msg_mock = get_telegram_testobject(mocker, default_conf)
mocker.patch('freqtrade.rpc.telegram.Telegram._send_inline_msg', inline_msg_mock)
return telegram, ftbot, msg_mock, inline_msg_mock
def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None):
msg_mock = MagicMock()
if mock:
@@ -902,6 +911,33 @@ def test_forcebuy_handle_exception(default_conf, update, mocker) -> None:
assert msg_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.'
def test_forcebuy_no_pair(default_conf, update, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
fbuy_mock = MagicMock(return_value=None)
mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
telegram, freqtradebot, _, inline_msg_mock = get_telegram_testobject_with_inline(mocker,
default_conf)
patch_get_signal(freqtradebot, (True, False))
context = MagicMock()
context.args = []
telegram._forcebuy(update=update, context=context)
assert fbuy_mock.call_count == 0
assert inline_msg_mock.call_count == 1
assert inline_msg_mock.call_args_list[0][0][0] == 'Which pair?'
assert inline_msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy'
keyboard = inline_msg_mock.call_args_list[0][1]['keyboard']
assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4
update = MagicMock()
update.callback_query = MagicMock()
update.callback_query.data = 'XRP/USDT'
telegram._forcebuy_inline(update, None)
assert fbuy_mock.call_count == 1
def test_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
@@ -929,7 +965,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
telegram._performance(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>ETH/BTC\t6.20% (1)</code>' in msg_mock.call_args_list[0][0][0]
assert '<code>ETH/BTC\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
@@ -969,6 +1005,11 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram._locks(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'No active locks.' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason')
PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef')
@@ -1102,6 +1143,15 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
assert '<b>Edge only validated following pairs:</b>\n<pre>' in msg_mock.call_args_list[0][0][0]
assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
return_value={}))
telegram._edge(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert '<b>Edge only validated following pairs:</b>' in msg_mock.call_args_list[0][0][0]
assert 'Winrate' not in msg_mock.call_args_list[0][0][0]
def test_telegram_trades(mocker, update, default_conf, fee):

View File

@@ -36,9 +36,11 @@ def test_default_strategy(result, fee):
)
assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc') is True
rate=20000, time_in_force='gtc',
current_time=datetime.utcnow()) is True
assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc', sell_reason='roi') is True
rate=20000, time_in_force='gtc', sell_reason='roi',
current_time=datetime.utcnow()) is True
assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(),
current_rate=20_000, current_profit=0.05) == strategy.stoploss

View File

@@ -382,6 +382,50 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
strategy.custom_stoploss = original_stopvalue
def test_custom_sell(default_conf, fee, caplog) -> None:
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
trade = Trade(
pair='ETH/BTC',
stake_amount=0.01,
amount=1,
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
open_rate=1,
)
now = arrow.utcnow().datetime
res = strategy.should_sell(trade, 1, now, False, False, None, None, 0)
assert res.sell_flag is False
assert res.sell_type == SellType.NONE
strategy.custom_sell = MagicMock(return_value=True)
res = strategy.should_sell(trade, 1, now, False, False, None, None, 0)
assert res.sell_flag is True
assert res.sell_type == SellType.CUSTOM_SELL
assert res.sell_reason == 'custom_sell'
strategy.custom_sell = MagicMock(return_value='hello world')
res = strategy.should_sell(trade, 1, now, False, False, None, None, 0)
assert res.sell_type == SellType.CUSTOM_SELL
assert res.sell_flag is True
assert res.sell_reason == 'hello world'
caplog.clear()
strategy.custom_sell = MagicMock(return_value='h' * 100)
res = strategy.should_sell(trade, 1, now, False, False, None, None, 0)
assert res.sell_type == SellType.CUSTOM_SELL
assert res.sell_flag is True
assert res.sell_reason == 'h' * 64
assert log_has_re('Custom sell reason returned from custom_sell is too long.*', caplog)
def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x)
@@ -592,7 +636,7 @@ def test_hyperopt_parameters():
assert len(list(intpar.range)) == 1
# Range contains ONLY the default / value.
assert list(intpar.range) == [intpar.value]
intpar.hyperopt = True
intpar.in_space = True
assert len(list(intpar.range)) == 6
assert list(intpar.range) == [0, 1, 2, 3, 4, 5]
@@ -627,4 +671,4 @@ def test_auto_hyperopt_interface(default_conf):
strategy.sell_rsi = IntParameter([0, 10], default=5, space='buy')
with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"):
[x for x in strategy.enumerate_parameters('sell')]
[x for x in strategy._detect_parameters('sell')]

View File

@@ -1371,7 +1371,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
}
mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order',
side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging)
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order',
return_value=stoploss_order_hanging)
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
@@ -1959,7 +1960,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open,
# if ROI is reached we must sell
patch_get_signal(freqtrade, value=(False, True))
assert freqtrade.handle_trade(trade)
assert log_has("ETH/BTC - Required profit reached. sell_flag=True, sell_type=SellType.ROI",
assert log_has("ETH/BTC - Required profit reached. sell_type=SellType.ROI",
caplog)
@@ -1988,7 +1989,7 @@ def test_handle_trade_use_sell_signal(
patch_get_signal(freqtrade, value=(False, True))
assert freqtrade.handle_trade(trade)
assert log_has("ETH/BTC - Sell signal received. sell_flag=True, sell_type=SellType.SELL_SIGNAL",
assert log_has("ETH/BTC - Sell signal received. sell_type=SellType.SELL_SIGNAL",
caplog)
@@ -2430,13 +2431,22 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non
freqtrade._notify_buy_cancel = MagicMock()
trade = MagicMock()
trade.pair = 'LTC/ETH'
trade.pair = 'LTC/USDT'
trade.open_rate = 200
limit_buy_order['filled'] = 0.0
limit_buy_order['status'] = 'open'
reason = CANCEL_REASON['TIMEOUT']
assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 1
cancel_order_mock.reset_mock()
caplog.clear()
limit_buy_order['filled'] = 0.01
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 0
assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog)
caplog.clear()
cancel_order_mock.reset_mock()
limit_buy_order['filled'] = 2
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
@@ -2491,7 +2501,8 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
freqtrade._notify_buy_cancel = MagicMock()
trade = MagicMock()
trade.pair = 'LTC/ETH'
trade.pair = 'LTC/USDT'
trade.open_rate = 200
limit_buy_order['filled'] = 0.0
limit_buy_order['status'] = 'open'
reason = CANCEL_REASON['TIMEOUT']
@@ -2592,14 +2603,16 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
fetch_ticker=ticker_sell_up
)
# Prevented sell ...
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI)
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI))
assert rpc_mock.call_count == 0
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
# Repatch with true
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI)
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI))
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
assert rpc_mock.call_count == 1
@@ -2651,7 +2664,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
)
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
sell_reason=SellType.STOP_LOSS)
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
assert rpc_mock.call_count == 2
last_msg = rpc_mock.call_args_list[-1][0][0]
@@ -2708,7 +2721,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
trade.stop_loss = 0.00001099 * 0.99
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
sell_reason=SellType.STOP_LOSS)
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
assert rpc_mock.call_count == 2
last_msg = rpc_mock.call_args_list[-1][0][0]
@@ -2760,7 +2773,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c
trade.stoploss_order_id = "abcd"
freqtrade.execute_sell(trade=trade, limit=1234,
sell_reason=SellType.STOP_LOSS)
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
assert sellmock.call_count == 1
assert log_has('Could not cancel stoploss order abcd', caplog)
@@ -2810,7 +2823,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
)
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
sell_reason=SellType.SELL_SIGNAL)
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
trade = Trade.query.first()
assert trade
@@ -2915,7 +2928,8 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
)
freqtrade.config['order_types']['sell'] = 'market'
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI)
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI))
assert not trade.is_open
assert trade.close_profit == 0.0620716
@@ -2969,8 +2983,9 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee,
fetch_ticker=ticker_sell_up
)
sell_reason = SellCheckTuple(sell_type=SellType.ROI)
assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
sell_reason=SellType.ROI)
sell_reason=sell_reason)
assert mock_insuf.call_count == 1
@@ -3063,7 +3078,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_o
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple(
sell_flag=False, sell_type=SellType.NONE))
sell_type=SellType.NONE))
freqtrade.enter_positions()
trade = Trade.query.first()
@@ -3212,7 +3227,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo
)
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
sell_reason=SellType.STOP_LOSS)
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
trade.close(ticker_sell_down()['bid'])
assert freqtrade.strategy.is_pair_locked(trade.pair)

View File

@@ -51,8 +51,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
# Sell 3rd trade (not called for the first trade)
should_sell_mock = MagicMock(side_effect=[
SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)]
SellCheckTuple(sell_type=SellType.NONE),
SellCheckTuple(sell_type=SellType.SELL_SIGNAL)]
)
cancel_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
@@ -63,7 +63,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y,
fetch_stoploss_order=stoploss_order_mock,
cancel_stoploss_order=cancel_order_mock,
cancel_stoploss_order_with_result=cancel_order_mock,
)
mocker.patch.multiple(
@@ -156,11 +156,11 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
_notify_sell=MagicMock(),
)
should_sell_mock = MagicMock(side_effect=[
SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL),
SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
SellCheckTuple(sell_flag=None, sell_type=SellType.NONE)]
SellCheckTuple(sell_type=SellType.NONE),
SellCheckTuple(sell_type=SellType.SELL_SIGNAL),
SellCheckTuple(sell_type=SellType.NONE),
SellCheckTuple(sell_type=SellType.NONE),
SellCheckTuple(sell_type=SellType.NONE)]
)
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)

View File

@@ -7,7 +7,7 @@ from unittest.mock import MagicMock
import arrow
import pytest
from sqlalchemy import create_engine
from sqlalchemy import create_engine, inspect
from freqtrade import constants
from freqtrade.exceptions import DependencyException, OperationalException
@@ -627,6 +627,63 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert orders[1].order_id == 'stop_order_id222'
assert orders[1].ft_order_side == 'stoploss'
caplog.clear()
# Drop latest column
engine.execute("alter table orders rename to orders_bak")
inspector = inspect(engine)
for index in inspector.get_indexes('orders_bak'):
engine.execute(f"drop index {index['name']}")
# Recreate table
engine.execute("""
CREATE TABLE orders (
id INTEGER NOT NULL,
ft_trade_id INTEGER,
ft_order_side VARCHAR NOT NULL,
ft_pair VARCHAR NOT NULL,
ft_is_open BOOLEAN NOT NULL,
order_id VARCHAR NOT NULL,
status VARCHAR,
symbol VARCHAR,
order_type VARCHAR,
side VARCHAR,
price FLOAT,
amount FLOAT,
filled FLOAT,
remaining FLOAT,
cost FLOAT,
order_date DATETIME,
order_filled_date DATETIME,
order_update_date DATETIME,
PRIMARY KEY (id),
CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id),
FOREIGN KEY(ft_trade_id) REFERENCES trades (id)
)
""")
engine.execute("""
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status,
symbol, order_type, side, price, amount, filled, remaining, cost, order_date,
order_filled_date, order_update_date)
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status,
symbol, order_type, side, price, amount, filled, remaining, cost, order_date,
order_filled_date, order_update_date
from orders_bak
""")
# Run init to test migration
init_db(default_conf['db_url'], default_conf['dry_run'])
assert log_has("trying orders_bak1", caplog)
orders = Order.query.all()
assert len(orders) == 2
assert orders[0].order_id == 'buy_order'
assert orders[0].ft_order_side == 'buy'
assert orders[1].order_id == 'stop_order_id222'
assert orders[1].ft_order_side == 'stoploss'
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
"""
@@ -774,11 +831,16 @@ def test_adjust_min_max_rates(fee):
@pytest.mark.usefixtures("init_persistence")
def test_get_open(fee):
@pytest.mark.parametrize('use_db', [True, False])
def test_get_open(fee, use_db):
Trade.use_db = use_db
Trade.reset_trades()
create_mock_trades(fee)
create_mock_trades(fee, use_db)
assert len(Trade.get_open_trades()) == 4
Trade.use_db = True
@pytest.mark.usefixtures("init_persistence")
def test_to_json(default_conf, fee):
@@ -1083,6 +1145,13 @@ def test_get_trades_proxy(fee, use_db):
Trade.use_db = True
def test_get_trades_backtest():
Trade.use_db = False
with pytest.raises(NotImplementedError, match=r"`Trade.get_trades\(\)` not .*"):
Trade.get_trades([])
Trade.use_db = True
@pytest.mark.usefixtures("init_persistence")
def test_get_overall_performance(fee):
@@ -1216,11 +1285,24 @@ def test_Trade_object_idem():
trade = vars(Trade)
localtrade = vars(LocalTrade)
excludes = (
'delete',
'session',
'query',
'open_date',
'get_best_pair',
'get_overall_performance',
'total_open_trades_stakes',
'get_sold_trades_without_assigned_fees',
'get_open_trades_without_assigned_fees',
'get_open_order_trades',
'get_trades',
)
# Parent (LocalTrade) should have the same attributes
for item in trade:
# Exclude private attributes and open_date (as it's not assigned a default)
if (not item.startswith('_')
and item not in ('delete', 'session', 'query', 'open_date')):
if (not item.startswith('_') and item not in excludes):
assert item in localtrade
# Fails if only a column is added without corresponding parent field

Binary file not shown.

File diff suppressed because one or more lines are too long